@scrypted/server 0.1.15 → 0.2.2

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 (54) hide show
  1. package/dist/event-registry.js +3 -4
  2. package/dist/event-registry.js.map +1 -1
  3. package/dist/http-interfaces.js +11 -0
  4. package/dist/http-interfaces.js.map +1 -1
  5. package/dist/plugin/media.js +77 -66
  6. package/dist/plugin/media.js.map +1 -1
  7. package/dist/plugin/plugin-api.js +1 -1
  8. package/dist/plugin/plugin-api.js.map +1 -1
  9. package/dist/plugin/plugin-device.js +25 -11
  10. package/dist/plugin/plugin-device.js.map +1 -1
  11. package/dist/plugin/plugin-host-api.js.map +1 -1
  12. package/dist/plugin/plugin-host.js +11 -6
  13. package/dist/plugin/plugin-host.js.map +1 -1
  14. package/dist/plugin/plugin-http.js +1 -1
  15. package/dist/plugin/plugin-http.js.map +1 -1
  16. package/dist/plugin/plugin-remote-worker.js +170 -17
  17. package/dist/plugin/plugin-remote-worker.js.map +1 -1
  18. package/dist/plugin/plugin-remote.js +25 -85
  19. package/dist/plugin/plugin-remote.js.map +1 -1
  20. package/dist/plugin/runtime/node-fork-worker.js +11 -3
  21. package/dist/plugin/runtime/node-fork-worker.js.map +1 -1
  22. package/dist/plugin/socket-serializer.js +17 -0
  23. package/dist/plugin/socket-serializer.js.map +1 -0
  24. package/dist/rpc.js +3 -3
  25. package/dist/rpc.js.map +1 -1
  26. package/dist/runtime.js +14 -11
  27. package/dist/runtime.js.map +1 -1
  28. package/dist/scrypted-plugin-main.js +4 -1
  29. package/dist/scrypted-plugin-main.js.map +1 -1
  30. package/dist/scrypted-server-main.js +53 -12
  31. package/dist/scrypted-server-main.js.map +1 -1
  32. package/dist/server-settings.js +5 -1
  33. package/dist/server-settings.js.map +1 -1
  34. package/dist/state.js +2 -1
  35. package/dist/state.js.map +1 -1
  36. package/package.json +5 -11
  37. package/src/event-registry.ts +3 -4
  38. package/src/http-interfaces.ts +13 -0
  39. package/src/plugin/media.ts +93 -74
  40. package/src/plugin/plugin-api.ts +5 -4
  41. package/src/plugin/plugin-device.ts +25 -11
  42. package/src/plugin/plugin-host-api.ts +1 -1
  43. package/src/plugin/plugin-host.ts +6 -5
  44. package/src/plugin/plugin-http.ts +2 -2
  45. package/src/plugin/plugin-remote-worker.ts +211 -23
  46. package/src/plugin/plugin-remote.ts +31 -94
  47. package/src/plugin/runtime/node-fork-worker.ts +11 -3
  48. package/src/plugin/runtime/runtime-worker.ts +1 -1
  49. package/src/plugin/socket-serializer.ts +15 -0
  50. package/src/rpc.ts +3 -2
  51. package/src/runtime.ts +10 -10
  52. package/src/scrypted-plugin-main.ts +4 -1
  53. package/src/scrypted-server-main.ts +59 -13
  54. package/src/state.ts +2 -1
@@ -216,15 +216,24 @@ export class PluginDeviceProxyHandler implements PrimitiveProxyHandler<any>, Scr
216
216
  let { allInterfaces } = await previousEntry;
217
217
  try {
218
218
  const mixinProvider = this.scrypted.getDevice(mixinId) as ScryptedDevice & MixinProvider;
219
- const interfaces = mixinProvider?.interfaces?.includes(ScryptedInterface.MixinProvider) && await mixinProvider?.canMixin(type, allInterfaces) as any as ScryptedInterface[];
219
+ const isMixinProvider = mixinProvider?.interfaces?.includes(ScryptedInterface.MixinProvider);
220
+ const interfaces = isMixinProvider && await mixinProvider?.canMixin(type, allInterfaces) as any as ScryptedInterface[];
220
221
  if (!interfaces) {
221
222
  // this is not an error
222
223
  // do not advertise interfaces so it is skipped during
223
224
  // vtable lookup.
224
- console.log(`mixin provider ${mixinId} can no longer mixin ${this.id}`);
225
- const mixins: string[] = getState(pluginDevice, ScryptedInterfaceProperty.mixins) || [];
226
- this.scrypted.stateManager.setPluginDeviceState(pluginDevice, ScryptedInterfaceProperty.mixins, mixins.filter(mid => mid !== mixinId))
227
- this.scrypted.datastore.upsert(pluginDevice);
225
+ if (!mixinProvider || (isMixinProvider && !interfaces)) {
226
+ console.log(`Mixin provider ${mixinId} can no longer mixin ${this.id}. Removing.`, {
227
+ mixinProvider: !!mixinProvider,
228
+ interfaces,
229
+ });
230
+ const mixins: string[] = getState(pluginDevice, ScryptedInterfaceProperty.mixins) || [];
231
+ this.scrypted.stateManager.setPluginDeviceState(pluginDevice, ScryptedInterfaceProperty.mixins, mixins.filter(mid => mid !== mixinId))
232
+ this.scrypted.datastore.upsert(pluginDevice);
233
+ }
234
+ else {
235
+ console.log(`Mixin provider ${mixinId} can not mixin ${this.id}. It is no longer a MixinProvider. This may be temporary. Passing through.`);
236
+ }
228
237
  return {
229
238
  passthrough: true,
230
239
  allInterfaces,
@@ -353,12 +362,17 @@ export class PluginDeviceProxyHandler implements PrimitiveProxyHandler<any>, Scr
353
362
  for (const mixin of this.mixinTable) {
354
363
  const entry = await mixin.entry;
355
364
  if (!entry.methods) {
356
- const pluginDevice = this.scrypted.findPluginDeviceById(mixin.mixinProviderId || this.id);
357
- const plugin = this.scrypted.plugins[pluginDevice.pluginId];
358
- let methods = new Set<string>(getInterfaceMethods(ScryptedInterfaceDescriptors, entry.interfaces))
359
- if (plugin.api.descriptors)
360
- methods = new Set<string>([...methods, ...getInterfaceMethods(plugin.api.descriptors, entry.interfaces)]);
361
- entry.methods = methods;
365
+ if (entry.interfaces.size) {
366
+ const pluginDevice = this.scrypted.findPluginDeviceById(mixin.mixinProviderId || this.id);
367
+ const plugin = this.scrypted.plugins[pluginDevice.pluginId];
368
+ let methods = new Set<string>(getInterfaceMethods(ScryptedInterfaceDescriptors, entry.interfaces))
369
+ if (plugin.api.descriptors)
370
+ methods = new Set<string>([...methods, ...getInterfaceMethods(plugin.api.descriptors, entry.interfaces)]);
371
+ entry.methods = methods;
372
+ }
373
+ else {
374
+ entry.methods = new Set();
375
+ }
362
376
  }
363
377
  if (entry.methods.has(method)) {
364
378
  return { mixin, entry };
@@ -47,7 +47,7 @@ export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAP
47
47
 
48
48
  // do we care about mixin validation here?
49
49
  // maybe to prevent/notify errant dangling events?
50
- async onMixinEvent(id: string, nativeIdOrMixinDevice: ScryptedNativeId | any, eventInterface: any, eventData?: any) {
50
+ async onMixinEvent(id: string, nativeIdOrMixinDevice: ScryptedNativeId | any, eventInterface: string, eventData?: any) {
51
51
  // nativeId code path has been deprecated in favor of mixin object 12/10/2021
52
52
  const device = this.scrypted.findPluginDeviceById(id);
53
53
 
@@ -135,7 +135,7 @@ export class PluginHost {
135
135
  this.io.on('connection', async (socket) => {
136
136
  try {
137
137
  try {
138
- if (socket.request.url.indexOf('/api') !== -1) {
138
+ if (socket.request.url.indexOf('/engine.io/api') !== -1) {
139
139
  if (socket.request.url.indexOf('/public') !== -1) {
140
140
  socket.close();
141
141
  return;
@@ -179,7 +179,7 @@ export class PluginHost {
179
179
 
180
180
  const { runtime } = this.packageJson.scrypted;
181
181
  const mediaManager = runtime === 'python'
182
- ? new MediaManagerHostImpl(pluginDeviceId, scrypted.stateManager.getSystemState(), console, id => scrypted.getDevice(id))
182
+ ? new MediaManagerHostImpl(pluginDeviceId, () => scrypted.stateManager.getSystemState(), console, id => scrypted.getDevice(id))
183
183
  : undefined;
184
184
 
185
185
  this.api = new PluginHostAPI(scrypted, this.pluginId, this, mediaManager);
@@ -214,7 +214,6 @@ export class PluginHost {
214
214
  await remote.setNativeId(pluginDevice.nativeId, pluginDevice._id, pluginDevice.storage || {});
215
215
  }
216
216
 
217
- await remote.setSystemState(scrypted.stateManager.getSystemState());
218
217
  const waitDebug = pluginDebug?.waitDebug;
219
218
  if (waitDebug) {
220
219
  console.info('waiting for debugger...');
@@ -374,7 +373,8 @@ export class PluginHost {
374
373
  serializer.setupRpcPeer(rpcPeer);
375
374
 
376
375
  // wrap the host api with a connection specific api that can be torn down on disconnect
377
- const api = new PluginAPIProxy(this.api, await this.peer.getParam('mediaManager'));
376
+ const createMediaManager = await this.peer.getParam('createMediaManager');
377
+ const api = new PluginAPIProxy(this.api, await createMediaManager());
378
378
  const kill = () => {
379
379
  serializer.onDisconnected();
380
380
  api.removeListeners();
@@ -389,7 +389,8 @@ export class PluginHost {
389
389
  const rpcPeer = createDuplexRpcPeer(`api/${this.pluginId}`, 'duplex', duplex, duplex);
390
390
 
391
391
  // wrap the host api with a connection specific api that can be torn down on disconnect
392
- const api = new PluginAPIProxy(this.api, await this.peer.getParam('mediaManager'));
392
+ const createMediaManager = await this.peer.getParam('createMediaManager');
393
+ const api = new PluginAPIProxy(this.api, await createMediaManager());
393
394
  const kill = () => {
394
395
  api.removeListeners();
395
396
  };
@@ -38,7 +38,7 @@ export abstract class PluginHttp<T> {
38
38
 
39
39
  abstract handleEngineIOEndpoint(req: Request, res: ServerResponse, endpointRequest: HttpRequest, pluginData: T): void;
40
40
  abstract handleRequestEndpoint(req: Request, res: Response, endpointRequest: HttpRequest, pluginData: T): void;
41
- abstract getEndpointPluginData(endpoint: string, isUpgrade: boolean, isEngineIOEndpoint: boolean): Promise<T>;
41
+ abstract getEndpointPluginData(req: Request, endpoint: string, isUpgrade: boolean, isEngineIOEndpoint: boolean): Promise<T>;
42
42
  abstract handleWebSocket(endpoint: string, httpRequest: HttpRequest, ws: WebSocket, pluginData: T): Promise<void>;
43
43
 
44
44
  async endpointHandler(req: Request, res: Response, isPublicEndpoint: boolean, isEngineIOEndpoint: boolean,
@@ -75,7 +75,7 @@ export abstract class PluginHttp<T> {
75
75
  return;
76
76
  }
77
77
 
78
- const pluginData = await this.getEndpointPluginData(endpoint, isUpgrade, isEngineIOEndpoint);
78
+ const pluginData = await this.getEndpointPluginData(req, endpoint, isUpgrade, isEngineIOEndpoint);
79
79
  if (!pluginData) {
80
80
  end(404, 'Not Found');
81
81
  return;
@@ -1,20 +1,32 @@
1
1
  import { DeviceManager, ScryptedNativeId, ScryptedStatic, SystemManager } from '@scrypted/types';
2
+ import AdmZip from 'adm-zip';
2
3
  import { Console } from 'console';
4
+ import fs from 'fs';
5
+ import { Volume } from 'memfs';
3
6
  import net from 'net';
7
+ import path from 'path';
4
8
  import { install as installSourceMapSupport } from 'source-map-support';
5
9
  import { PassThrough } from 'stream';
6
10
  import { RpcMessage, RpcPeer } from '../rpc';
7
11
  import { MediaManagerImpl } from './media';
8
- import { PluginAPI } from './plugin-api';
12
+ import { PluginAPI, PluginAPIProxy, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
9
13
  import { installOptionalDependencies } from './plugin-npm-dependencies';
10
- import { attachPluginRemote, PluginReader } from './plugin-remote';
14
+ import { attachPluginRemote, DeviceManagerImpl, PluginReader, setupPluginRemote } from './plugin-remote';
11
15
  import { createREPLServer } from './plugin-repl';
16
+ import { NodeThreadWorker } from './runtime/node-thread-worker';
17
+ const { link } = require('linkfs');
12
18
 
13
- export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessage, reject?: (e: Error) => void) => void) {
19
+ interface PluginStats {
20
+ type: 'stats',
21
+ cpu: NodeJS.CpuUsage;
22
+ memoryUsage: NodeJS.MemoryUsage;
23
+ }
24
+
25
+ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessage, reject?: (e: Error) => void, serializationContext?: any) => void) {
14
26
  const peer = new RpcPeer('unknown', 'host', peerSend);
15
27
 
16
28
  let systemManager: SystemManager;
17
- let deviceManager: DeviceManager;
29
+ let deviceManager: DeviceManagerImpl;
18
30
  let api: PluginAPI;
19
31
 
20
32
  const getConsole = (hook: (stdout: PassThrough, stderr: PassThrough) => Promise<void>,
@@ -66,13 +78,18 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
66
78
  return pluginsPromise;
67
79
  }
68
80
 
81
+ const deviceConsoles = new Map<string, Console>();
69
82
  const getDeviceConsole = (nativeId?: ScryptedNativeId) => {
70
83
  // the the plugin console is simply the default console
71
84
  // and gets read from stderr/stdout.
72
85
  if (!nativeId)
73
86
  return console;
74
87
 
75
- return getConsole(async (stdout, stderr) => {
88
+ let ret = deviceConsoles.get(nativeId);
89
+ if (ret)
90
+ return ret;
91
+
92
+ ret = getConsole(async (stdout, stderr) => {
76
93
  const connect = async () => {
77
94
  const plugins = await getPlugins();
78
95
  const port = await plugins.getRemoteServicePort(peer.selfName, 'console-writer');
@@ -93,19 +110,28 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
93
110
  };
94
111
  connect();
95
112
  }, undefined, undefined);
113
+
114
+ deviceConsoles.set(nativeId, ret);
115
+ return ret;
96
116
  }
97
117
 
118
+ const mixinConsoles = new Map<string, Map<string, Console>>();
119
+
98
120
  const getMixinConsole = (mixinId: string, nativeId: ScryptedNativeId) => {
99
- return getConsole(async (stdout, stderr) => {
121
+ let nativeIdConsoles = mixinConsoles.get(nativeId);
122
+ if (!nativeIdConsoles) {
123
+ nativeIdConsoles = new Map();
124
+ mixinConsoles.set(nativeId, nativeIdConsoles);
125
+ }
126
+
127
+ let ret = nativeIdConsoles.get(mixinId);
128
+ if (ret)
129
+ return ret;
130
+
131
+ ret = getConsole(async (stdout, stderr) => {
100
132
  if (!mixinId) {
101
133
  return;
102
134
  }
103
- // todo: fix this. a mixin provider can mixin another device to make it a mixin provider itself.
104
- // so the mixin id in the mixin table will be incorrect.
105
- // there's no easy way to fix this from the remote.
106
- // if (!systemManager.getDeviceById(mixinId).mixins.includes(idForNativeId(nativeId))) {
107
- // return;
108
- // }
109
135
  const reconnect = () => {
110
136
  stdout.removeAllListeners();
111
137
  stderr.removeAllListeners();
@@ -147,17 +173,40 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
147
173
  }
148
174
  tryConnect();
149
175
  }, getDeviceConsole(nativeId), `[${systemManager.getDeviceById(mixinId)?.name}]`);
176
+
177
+ nativeIdConsoles.set(mixinId, ret);
178
+ return ret;
150
179
  }
151
180
 
152
- peer.getParam('updateStats').then((updateStats: (stats: any) => void) => {
153
- let lastCpuUsage: NodeJS.CpuUsage;
181
+ // process.cpuUsage is for the entire process.
182
+ // process.memoryUsage is per thread.
183
+ const allMemoryStats = new Map<NodeThreadWorker, NodeJS.MemoryUsage>();
184
+
185
+ peer.getParam('updateStats').then((updateStats: (stats: PluginStats) => void) => {
154
186
  setInterval(() => {
155
- const cpuUsage = process.cpuUsage(lastCpuUsage);
156
- lastCpuUsage = cpuUsage;
187
+ const cpuUsage = process.cpuUsage();
188
+ allMemoryStats.set(undefined, process.memoryUsage());
189
+
190
+ const memoryUsage: NodeJS.MemoryUsage = {
191
+ rss: 0,
192
+ heapTotal: 0,
193
+ heapUsed: 0,
194
+ external: 0,
195
+ arrayBuffers: 0,
196
+ }
197
+
198
+ for (const mu of allMemoryStats.values()) {
199
+ memoryUsage.rss += mu.rss;
200
+ memoryUsage.heapTotal += mu.heapTotal;
201
+ memoryUsage.heapUsed += mu.heapUsed;
202
+ memoryUsage.external += mu.external;
203
+ memoryUsage.arrayBuffers += mu.arrayBuffers;
204
+ }
205
+
157
206
  updateStats({
158
207
  type: 'stats',
159
208
  cpu: cpuUsage,
160
- memoryUsage: process.memoryUsage(),
209
+ memoryUsage,
161
210
  });
162
211
  }, 10000);
163
212
  });
@@ -183,10 +232,6 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
183
232
  api = _api;
184
233
  peer.selfName = pluginId;
185
234
  },
186
- onPluginReady: async (scrypted, params, plugin) => {
187
- replPort = createREPLServer(scrypted, params, plugin);
188
- postInstallSourceMapSupport(scrypted);
189
- },
190
235
  getPluginConsole,
191
236
  getDeviceConsole,
192
237
  getMixinConsole,
@@ -198,7 +243,59 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
198
243
  }
199
244
  throw new Error(`unknown service ${name}`);
200
245
  },
201
- async onLoadZip(pluginReader: PluginReader, packageJson: any) {
246
+ async onLoadZip(scrypted: ScryptedStatic, params: any, packageJson: any, zipData: Buffer | string, zipOptions?: PluginRemoteLoadZipOptions) {
247
+ let volume: any;
248
+ let pluginReader: PluginReader;
249
+ if (zipOptions?.unzippedPath && fs.existsSync(zipOptions?.unzippedPath)) {
250
+ volume = link(fs, ['', path.join(zipOptions.unzippedPath, 'fs')]);
251
+ pluginReader = name => {
252
+ const filename = path.join(zipOptions.unzippedPath, name);
253
+ if (!fs.existsSync(filename))
254
+ return;
255
+ return fs.readFileSync(filename);
256
+ };
257
+ }
258
+ else {
259
+ const admZip = new AdmZip(zipData);
260
+ volume = new Volume();
261
+ for (const entry of admZip.getEntries()) {
262
+ if (entry.isDirectory)
263
+ continue;
264
+ if (!entry.entryName.startsWith('fs/'))
265
+ continue;
266
+ const name = entry.entryName.substring('fs/'.length);
267
+ volume.mkdirpSync(path.dirname(name));
268
+ const data = entry.getData();
269
+ volume.writeFileSync(name, data);
270
+ }
271
+
272
+ pluginReader = name => {
273
+ const entry = admZip.getEntry(name);
274
+ if (!entry)
275
+ return;
276
+ return entry.getData();
277
+ }
278
+ }
279
+ zipData = undefined;
280
+
281
+ const pluginConsole = getPluginConsole?.();
282
+ params.console = pluginConsole;
283
+ params.require = (name: string) => {
284
+ if (name === 'fakefs' || (name === 'fs' && !packageJson.scrypted.realfs)) {
285
+ return volume;
286
+ }
287
+ if (name === 'realfs') {
288
+ return require('fs');
289
+ }
290
+ const module = require(name);
291
+ return module;
292
+ };
293
+ const window: any = {};
294
+ const exports: any = window;
295
+ window.exports = exports;
296
+ params.window = window;
297
+ params.exports = exports;
298
+
202
299
  const entry = pluginReader('main.nodejs.js.map')
203
300
  const map = entry?.toString();
204
301
 
@@ -234,10 +331,101 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
234
331
  };
235
332
 
236
333
  await installOptionalDependencies(getPluginConsole(), packageJson);
334
+
335
+ const main = pluginReader('main.nodejs.js');
336
+ pluginReader = undefined;
337
+ const script = main.toString();
338
+
339
+
340
+ const forks = new Set<PluginRemote>();
341
+
342
+ scrypted.fork = () => {
343
+ const ntw = new NodeThreadWorker(pluginId, {
344
+ env: process.env,
345
+ pluginDebug: undefined,
346
+ });
347
+
348
+ const result = (async () => {
349
+ const threadPeer = new RpcPeer('main', 'thread', (message, reject) => ntw.send(message, reject));
350
+ threadPeer.params.updateStats = (stats: PluginStats) => {
351
+ allMemoryStats.set(ntw, stats.memoryUsage);
352
+ }
353
+ ntw.setupRpcPeer(threadPeer);
354
+
355
+ class PluginForkAPI extends PluginAPIProxy {
356
+ [RpcPeer.PROPERTY_PROXY_ONEWAY_METHODS] = (api as any)[RpcPeer.PROPERTY_PROXY_ONEWAY_METHODS];
357
+
358
+ setStorage(nativeId: string, storage: { [key: string]: any; }): Promise<void> {
359
+ const id = deviceManager.nativeIds.get(nativeId).id;
360
+ (scrypted.pluginRemoteAPI as PluginRemote).setNativeId(nativeId, id, storage);
361
+ for (const r of forks) {
362
+ if (r === remote)
363
+ continue;
364
+ r.setNativeId(nativeId, id, storage);
365
+ }
366
+ return super.setStorage(nativeId, storage);
367
+ }
368
+ }
369
+ const forkApi = new PluginForkAPI(api);
370
+
371
+ const remote = await setupPluginRemote(threadPeer, forkApi, pluginId, () => systemManager.getSystemState());
372
+ forks.add(remote);
373
+ ntw.worker.on('exit', () => {
374
+ forkApi.removeListeners();
375
+ forks.delete(remote);
376
+ allMemoryStats.delete(ntw);
377
+ });
378
+
379
+ for (const [nativeId, dmd] of deviceManager.nativeIds.entries()) {
380
+ await remote.setNativeId(nativeId, dmd.id, dmd.storage);
381
+ }
382
+
383
+ const forkOptions = Object.assign({}, zipOptions);
384
+ forkOptions.fork = true;
385
+ return remote.loadZip(packageJson, zipData, forkOptions)
386
+ })();
387
+
388
+ result.catch(() => ntw.kill());
389
+
390
+ return {
391
+ worker: ntw.worker,
392
+ result,
393
+ }
394
+ }
395
+
396
+ try {
397
+ peer.evalLocal(script, zipOptions?.filename || '/plugin/main.nodejs.js', params);
398
+
399
+ if (zipOptions?.fork) {
400
+ pluginConsole?.log('plugin forked');
401
+ const fork = exports.fork;
402
+ const forked = await fork();
403
+ forked[RpcPeer.PROPERTY_JSON_DISABLE_SERIALIZATION] = true;
404
+ return forked;
405
+ }
406
+
407
+ pluginConsole?.log('plugin loaded');
408
+ let pluginInstance = exports.default;
409
+ // support exporting a plugin class, plugin main function,
410
+ // or a plugin instance
411
+ if (pluginInstance.toString().startsWith('class '))
412
+ pluginInstance = new pluginInstance();
413
+ if (typeof pluginInstance === 'function')
414
+ pluginInstance = await pluginInstance();
415
+
416
+ replPort = createREPLServer(scrypted, params, pluginInstance);
417
+ postInstallSourceMapSupport(scrypted);
418
+
419
+ return pluginInstance;
420
+ }
421
+ catch (e) {
422
+ pluginConsole?.error('plugin failed to start', e);
423
+ throw e;
424
+ }
237
425
  }
238
426
  }).then(scrypted => {
239
427
  systemManager = scrypted.systemManager;
240
- deviceManager = scrypted.deviceManager;
428
+ deviceManager = scrypted.deviceManager as DeviceManagerImpl;
241
429
  });
242
430
 
243
431
  return peer;
@@ -1,15 +1,10 @@
1
- import AdmZip from 'adm-zip';
2
- import { Volume } from 'memfs';
3
- import path from 'path';
4
- import { ScryptedNativeId, DeviceManager, Logger, Device, DeviceManifest, DeviceState, EndpointManager, SystemDeviceState, ScryptedStatic, SystemManager, MediaManager, ScryptedMimeTypes, ScryptedInterface, ScryptedInterfaceProperty, HttpRequest } from '@scrypted/types'
5
- import { PluginAPI, PluginLogger, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
6
- import { SystemManagerImpl } from './system';
1
+ import { Device, DeviceManager, DeviceManifest, DeviceState, EndpointManager, HttpRequest, Logger, MediaManager, ScryptedInterface, ScryptedInterfaceProperty, ScryptedMimeTypes, ScryptedNativeId, ScryptedStatic, SystemDeviceState, SystemManager } from '@scrypted/types';
7
2
  import { RpcPeer, RPCResultError } from '../rpc';
8
3
  import { BufferSerializer } from './buffer-serializer';
4
+ import { PluginAPI, PluginLogger, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
9
5
  import { createWebSocketClass, WebSocketConnectCallbacks, WebSocketMethods } from './plugin-remote-websocket';
10
- import fs from 'fs';
11
6
  import { checkProperty } from './plugin-state-check';
12
- const { link } = require('linkfs');
7
+ import { SystemManagerImpl } from './system';
13
8
 
14
9
  class DeviceLogger implements Logger {
15
10
  nativeId: ScryptedNativeId;
@@ -112,6 +107,10 @@ class DeviceStateProxyHandler implements ProxyHandler<any> {
112
107
  get?(target: any, p: PropertyKey, receiver: any) {
113
108
  if (p === 'id')
114
109
  return this.id;
110
+ if (p === RpcPeer.PROPERTY_PROXY_PROPERTIES)
111
+ return { id: this.id }
112
+ if (p === 'setState')
113
+ return this.setState;
115
114
  return this.deviceManager.systemManager.state[this.id][p as string]?.value;
116
115
  }
117
116
 
@@ -133,7 +132,7 @@ interface DeviceManagerDevice {
133
132
  storage: { [key: string]: any };
134
133
  }
135
134
 
136
- class DeviceManagerImpl implements DeviceManager {
135
+ export class DeviceManagerImpl implements DeviceManager {
137
136
  api: PluginAPI;
138
137
  nativeIds = new Map<string, DeviceManagerDevice>();
139
138
  deviceStorage = new Map<string, StorageImpl>();
@@ -158,6 +157,11 @@ class DeviceManagerImpl implements DeviceManager {
158
157
  return new Proxy(handler, handler);
159
158
  }
160
159
 
160
+ createDeviceState(id: string, setState: (property: string, value: any) => Promise<void>): DeviceState {
161
+ const handler = new DeviceStateProxyHandler(this, id, setState);
162
+ return new Proxy(handler, handler);
163
+ }
164
+
161
165
  getDeviceStorage(nativeId?: any): StorageImpl {
162
166
  let ret = this.deviceStorage.get(nativeId);
163
167
  if (!ret) {
@@ -302,7 +306,7 @@ export async function setupPluginRemote(peer: RpcPeer, api: PluginAPI, pluginId:
302
306
  if (!peer.constructorSerializerMap.get(Buffer))
303
307
  peer.addSerializer(Buffer, 'Buffer', new BufferSerializer());
304
308
  const getRemote = await peer.getParam('getRemote');
305
- const remote = await getRemote(api, pluginId);
309
+ const remote = await getRemote(api, pluginId) as PluginRemote;
306
310
 
307
311
  await remote.setSystemState(getSystemState());
308
312
  api.listen((id, eventDetails, eventData) => {
@@ -342,18 +346,17 @@ export interface WebSocketCustomHandler {
342
346
  export type PluginReader = (name: string) => Buffer;
343
347
 
344
348
  export interface PluginRemoteAttachOptions {
345
- createMediaManager?: (systemManager: SystemManager, deviceManager: DeviceManager) => Promise<MediaManager>;
349
+ createMediaManager?: (systemManager: SystemManager, deviceManager: DeviceManagerImpl) => Promise<MediaManager>;
346
350
  getServicePort?: (name: string, ...args: any[]) => Promise<number>;
347
351
  getDeviceConsole?: (nativeId?: ScryptedNativeId) => Console;
348
352
  getPluginConsole?: () => Console;
349
353
  getMixinConsole?: (id: string, nativeId?: ScryptedNativeId) => Console;
350
- onLoadZip?: (pluginReader: PluginReader, packageJson: any) => Promise<void>;
354
+ onLoadZip?: (scrypted: ScryptedStatic, params: any, packageJson: any, zipData: Buffer | string, zipOptions?: PluginRemoteLoadZipOptions) => Promise<any>;
351
355
  onGetRemote?: (api: PluginAPI, pluginId: string) => Promise<void>;
352
- onPluginReady?: (scrypted: ScryptedStatic, params: any, plugin: any) => Promise<void>;
353
356
  }
354
357
 
355
358
  export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOptions): Promise<ScryptedStatic> {
356
- const { createMediaManager, getServicePort, getDeviceConsole, getMixinConsole, getPluginConsole } = options || {};
359
+ const { createMediaManager, getServicePort, getDeviceConsole, getMixinConsole } = options || {};
357
360
 
358
361
  if (!peer.constructorSerializerMap.get(Buffer))
359
362
  peer.addSerializer(Buffer, 'Buffer', new BufferSerializer());
@@ -367,7 +370,11 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
367
370
  const systemManager = new SystemManagerImpl();
368
371
  const deviceManager = new DeviceManagerImpl(systemManager, getDeviceConsole, getMixinConsole);
369
372
  const endpointManager = new EndpointManagerImpl();
370
- const mediaManager = await api.getMediaManager() || await createMediaManager(systemManager, deviceManager);
373
+ const hostMediaManager = await api.getMediaManager();
374
+ if (!hostMediaManager) {
375
+ peer.params['createMediaManager'] = async () => createMediaManager(systemManager, deviceManager);
376
+ }
377
+ const mediaManager = hostMediaManager || await createMediaManager(systemManager, deviceManager);
371
378
  peer.params['mediaManager'] = mediaManager;
372
379
  const ioSockets: { [id: string]: WebSocketConnectCallbacks } = {};
373
380
 
@@ -382,6 +389,8 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
382
389
  endpointManager,
383
390
  mediaManager,
384
391
  log,
392
+ pluginHostAPI: api,
393
+ pluginRemoteAPI: undefined,
385
394
  }
386
395
 
387
396
  delete peer.params.getRemote;
@@ -403,9 +412,8 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
403
412
  'setNativeId',
404
413
  ],
405
414
  getServicePort,
406
- createDeviceState(id: string, setState: (property: string, value: any) => Promise<void>) {
407
- const handler = new DeviceStateProxyHandler(deviceManager, id, setState);
408
- return new Proxy(handler, handler);
415
+ async createDeviceState(id: string, setState: (property: string, value: any) => Promise<void>) {
416
+ return deviceManager.createDeviceState(id, setState);
409
417
  },
410
418
 
411
419
  async ioEvent(id: string, event: string, message?: any) {
@@ -472,50 +480,6 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
472
480
  },
473
481
 
474
482
  async loadZip(packageJson: any, zipData: Buffer | string, zipOptions?: PluginRemoteLoadZipOptions) {
475
- const pluginConsole = getPluginConsole?.();
476
-
477
- let volume: any;
478
- let pluginReader: PluginReader;
479
- if (zipOptions?.unzippedPath && fs.existsSync(zipOptions?.unzippedPath)) {
480
- volume = link(fs, ['', path.join(zipOptions.unzippedPath, 'fs')]);
481
- pluginReader = name => {
482
- const filename = path.join(zipOptions.unzippedPath, name);
483
- if (!fs.existsSync(filename))
484
- return;
485
- return fs.readFileSync(filename);
486
- };
487
- }
488
- else {
489
- const admZip = new AdmZip(zipData);
490
- volume = new Volume();
491
- for (const entry of admZip.getEntries()) {
492
- if (entry.isDirectory)
493
- continue;
494
- if (!entry.entryName.startsWith('fs/'))
495
- continue;
496
- const name = entry.entryName.substring('fs/'.length);
497
- volume.mkdirpSync(path.dirname(name));
498
- const data = entry.getData();
499
- volume.writeFileSync(name, data);
500
- }
501
-
502
- pluginReader = name => {
503
- const entry = admZip.getEntry(name);
504
- if (!entry)
505
- return;
506
- return entry.getData();
507
- }
508
- }
509
- zipData = undefined;
510
-
511
- await options?.onLoadZip?.(pluginReader, packageJson);
512
- const main = pluginReader('main.nodejs.js');
513
- pluginReader = undefined;
514
- const script = main.toString();
515
- const window: any = {};
516
- const exports: any = window;
517
- window.exports = exports;
518
-
519
483
 
520
484
  function websocketConnect(url: string, protocols: any, callbacks: WebSocketConnectCallbacks) {
521
485
  if (url.startsWith('io://') || url.startsWith('ws://')) {
@@ -535,18 +499,6 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
535
499
 
536
500
  const params: any = {
537
501
  __filename: undefined,
538
- exports,
539
- window,
540
- require: (name: string) => {
541
- if (name === 'fakefs' || (name === 'fs' && !packageJson.scrypted.realfs)) {
542
- return volume;
543
- }
544
- if (name === 'realfs') {
545
- return require('fs');
546
- }
547
- const module = require(name);
548
- return module;
549
- },
550
502
  deviceManager,
551
503
  systemManager,
552
504
  mediaManager,
@@ -555,32 +507,17 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
555
507
  localStorage,
556
508
  pluginHostAPI: api,
557
509
  WebSocket: createWebSocketClass(websocketConnect),
510
+ pluginRuntimeAPI: ret,
558
511
  };
559
512
 
560
- params.console = pluginConsole;
513
+ params.pluginRuntimeAPI = ret;
561
514
 
562
- try {
563
- peer.evalLocal(script, zipOptions?.filename || '/plugin/main.nodejs.js', params);
564
- pluginConsole?.log('plugin successfully loaded');
565
-
566
- let pluginInstance = exports.default;
567
- // support exporting a plugin class, plugin main function,
568
- // or a plugin instance
569
- if (pluginInstance.toString().startsWith('class '))
570
- pluginInstance = new pluginInstance();
571
- if (typeof pluginInstance === 'function')
572
- pluginInstance = await pluginInstance();
573
-
574
- await options?.onPluginReady?.(ret, params, pluginInstance);
575
- return pluginInstance;
576
- }
577
- catch (e) {
578
- pluginConsole?.error('plugin failed to start', e);
579
- throw e;
580
- }
515
+ return options.onLoadZip(ret, params, packageJson, zipData, zipOptions);
581
516
  },
582
517
  }
583
518
 
519
+ ret.pluginRemoteAPI = remote;
520
+
584
521
  return remote;
585
522
  }
586
523