@scrypted/server 0.1.16 → 0.2.5

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/event-registry.js +3 -4
  2. package/dist/event-registry.js.map +1 -1
  3. package/dist/plugin/media.js +51 -63
  4. package/dist/plugin/media.js.map +1 -1
  5. package/dist/plugin/plugin-api.js +1 -1
  6. package/dist/plugin/plugin-api.js.map +1 -1
  7. package/dist/plugin/plugin-device.js +29 -12
  8. package/dist/plugin/plugin-device.js.map +1 -1
  9. package/dist/plugin/plugin-host-api.js.map +1 -1
  10. package/dist/plugin/plugin-host.js +5 -2
  11. package/dist/plugin/plugin-host.js.map +1 -1
  12. package/dist/plugin/plugin-remote-worker.js +66 -24
  13. package/dist/plugin/plugin-remote-worker.js.map +1 -1
  14. package/dist/plugin/plugin-remote.js +14 -4
  15. package/dist/plugin/plugin-remote.js.map +1 -1
  16. package/dist/plugin/runtime/node-fork-worker.js +1 -1
  17. package/dist/plugin/runtime/node-fork-worker.js.map +1 -1
  18. package/dist/plugin/system.js +1 -1
  19. package/dist/plugin/system.js.map +1 -1
  20. package/dist/rpc.js +2 -2
  21. package/dist/rpc.js.map +1 -1
  22. package/dist/runtime.js +11 -16
  23. package/dist/runtime.js.map +1 -1
  24. package/dist/scrypted-server-main.js +8 -4
  25. package/dist/scrypted-server-main.js.map +1 -1
  26. package/dist/server-settings.js +5 -1
  27. package/dist/server-settings.js.map +1 -1
  28. package/dist/services/plugin.js +1 -0
  29. package/dist/services/plugin.js.map +1 -1
  30. package/dist/state.js +3 -2
  31. package/dist/state.js.map +1 -1
  32. package/package.json +6 -12
  33. package/scripts/github-workflow-publish-docker.sh +2 -0
  34. package/scripts/print-package-json-version.js +2 -0
  35. package/src/event-registry.ts +3 -4
  36. package/src/plugin/media.ts +69 -82
  37. package/src/plugin/plugin-api.ts +4 -4
  38. package/src/plugin/plugin-device.ts +29 -12
  39. package/src/plugin/plugin-host-api.ts +1 -1
  40. package/src/plugin/plugin-host.ts +0 -1
  41. package/src/plugin/plugin-remote-worker.ts +90 -30
  42. package/src/plugin/plugin-remote.ts +17 -6
  43. package/src/plugin/runtime/node-fork-worker.ts +1 -1
  44. package/src/plugin/system.ts +1 -1
  45. package/src/rpc.ts +2 -1
  46. package/src/runtime.ts +6 -16
  47. package/src/scrypted-server-main.ts +8 -4
  48. package/src/services/plugin.ts +1 -0
  49. package/src/state.ts +3 -2
@@ -1,7 +1,5 @@
1
- import { BufferConverter, BufferConvertorOptions, DeviceManager, FFmpegInput, MediaManager, MediaObject, MediaObjectOptions, MediaStreamUrl, ScryptedInterface, ScryptedInterfaceProperty, ScryptedMimeTypes, ScryptedNativeId, SystemDeviceState, SystemManager } from "@scrypted/types";
1
+ import { BufferConverter, DeviceManager, FFmpegInput, MediaManager, MediaObject, MediaObjectOptions, MediaStreamUrl, ScryptedInterface, ScryptedInterfaceProperty, ScryptedMimeTypes, ScryptedNativeId, SystemDeviceState, SystemManager } from "@scrypted/types";
2
2
  import axios from 'axios';
3
- import child_process from 'child_process';
4
- import { once } from 'events';
5
3
  import pathToFfmpeg from 'ffmpeg-static';
6
4
  import fs from 'fs';
7
5
  import https from 'https';
@@ -11,7 +9,6 @@ import Graph from 'node-dijkstra';
11
9
  import os from 'os';
12
10
  import path from 'path';
13
11
  import MimeType from 'whatwg-mimetype';
14
- import { safeKillFFmpeg } from '../media-helpers';
15
12
  import { MediaObjectRemote } from "./plugin-api";
16
13
 
17
14
  function typeMatches(target: string, candidate: string): boolean {
@@ -27,15 +24,28 @@ function mimeMatches(target: MimeType, candidate: MimeType) {
27
24
 
28
25
  const httpsAgent = new https.Agent({
29
26
  rejectUnauthorized: false
30
- })
27
+ });
28
+
29
+ type IdBufferConverter = BufferConverter & {
30
+ id: string;
31
+ };
32
+
33
+ function getBuiltinId(n: number) {
34
+ return 'builtin-' + n;
35
+ }
36
+
37
+ function getExtraId(n: number) {
38
+ return 'extra-' + n;
39
+ }
31
40
 
32
41
  export abstract class MediaManagerBase implements MediaManager {
33
- builtinConverters: BufferConverter[] = [];
34
- extraConverters: BufferConverter[] = [];
42
+ builtinConverters: IdBufferConverter[] = [];
43
+ extraConverters: IdBufferConverter[] = [];
35
44
 
36
45
  constructor() {
37
46
  for (const h of ['http', 'https']) {
38
47
  this.builtinConverters.push({
48
+ id: getBuiltinId(this.builtinConverters.length),
39
49
  fromMimeType: ScryptedMimeTypes.SchemePrefix + h,
40
50
  toMimeType: ScryptedMimeTypes.MediaObject,
41
51
  convert: async (data, fromMimeType, toMimeType) => {
@@ -51,18 +61,32 @@ export abstract class MediaManagerBase implements MediaManager {
51
61
  }
52
62
 
53
63
  this.builtinConverters.push({
64
+ id: getBuiltinId(this.builtinConverters.length),
54
65
  fromMimeType: ScryptedMimeTypes.SchemePrefix + 'file',
55
66
  toMimeType: ScryptedMimeTypes.MediaObject,
56
67
  convert: async (data, fromMimeType, toMimeType) => {
57
- const filename = data.toString();
68
+ const url = data.toString();
69
+ const filename = url.substring('file:'.length);
70
+
71
+ if (toMimeType === ScryptedMimeTypes.FFmpegInput) {
72
+ const ffmpegInput: FFmpegInput = {
73
+ url,
74
+ inputArguments: [
75
+ '-i', filename,
76
+ ]
77
+ };
78
+ return this.createFFmpegMediaObject(ffmpegInput);
79
+ }
80
+
58
81
  const ab = await fs.promises.readFile(filename);
59
- const mt = mimeType.lookup(data.toString());
82
+ const mt = mimeType.getType(data.toString());
60
83
  const mo = this.createMediaObject(ab, mt);
61
84
  return mo;
62
85
  }
63
86
  });
64
87
 
65
88
  this.builtinConverters.push({
89
+ id: getBuiltinId(this.builtinConverters.length),
66
90
  fromMimeType: ScryptedMimeTypes.Url,
67
91
  toMimeType: ScryptedMimeTypes.FFmpegInput,
68
92
  async convert(data, fromMimeType): Promise<Buffer> {
@@ -79,6 +103,7 @@ export abstract class MediaManagerBase implements MediaManager {
79
103
  });
80
104
 
81
105
  this.builtinConverters.push({
106
+ id: getBuiltinId(this.builtinConverters.length),
82
107
  fromMimeType: ScryptedMimeTypes.FFmpegInput,
83
108
  toMimeType: ScryptedMimeTypes.MediaStreamUrl,
84
109
  async convert(data: Buffer, fromMimeType: string): Promise<Buffer> {
@@ -87,6 +112,7 @@ export abstract class MediaManagerBase implements MediaManager {
87
112
  });
88
113
 
89
114
  this.builtinConverters.push({
115
+ id: getBuiltinId(this.builtinConverters.length),
90
116
  fromMimeType: ScryptedMimeTypes.MediaStreamUrl,
91
117
  toMimeType: ScryptedMimeTypes.FFmpegInput,
92
118
  async convert(data, fromMimeType: string): Promise<Buffer> {
@@ -111,66 +137,19 @@ export abstract class MediaManagerBase implements MediaManager {
111
137
  }
112
138
  });
113
139
 
140
+ // todo: move this to snapshot plugin
114
141
  this.builtinConverters.push({
142
+ id: getBuiltinId(this.builtinConverters.length),
115
143
  fromMimeType: 'image/*',
116
144
  toMimeType: 'image/*',
117
145
  convert: async (data, fromMimeType: string): Promise<Buffer> => {
118
146
  return data as Buffer;
119
147
  }
120
148
  });
121
-
122
- this.builtinConverters.push({
123
- fromMimeType: ScryptedMimeTypes.FFmpegInput,
124
- toMimeType: 'image/jpeg',
125
- convert: async (data, fromMimeType: string, toMimeType: string, options?: BufferConvertorOptions): Promise<Buffer> => {
126
- const console = this.getMixinConsole(options?.sourceId, undefined);
127
-
128
- const mt = new MimeType(toMimeType);
129
-
130
- const ffInput: FFmpegInput = JSON.parse(data.toString());
131
-
132
- const args = [
133
- '-hide_banner',
134
- '-y',
135
- ];
136
- args.push(...ffInput.inputArguments);
137
-
138
- const width = parseInt(mt.parameters.get('width'));
139
- const height = parseInt(mt.parameters.get('height'));
140
-
141
- if (mt.parameters.get('width') || mt.parameters.get('height')) {
142
- args.push(
143
- '-vf', `scale=${width || -1}:${height || -1}`,
144
- );
145
- }
146
-
147
- args.push("-vframes", "1", '-f', 'image2', 'pipe:3');
148
-
149
- const buffers: Buffer[] = [];
150
-
151
- const cp = child_process.spawn(await this.getFFmpegPath(), args, {
152
- stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
153
- });
154
- console.log('converting ffmpeg input to image.');
155
- // ffmpegLogInitialOutput(console, cp);
156
- cp.on('error', (code) => {
157
- console.error('ffmpeg error code', code);
158
- });
159
- cp.stdio[3].on('data', data => buffers.push(data));
160
- const to = setTimeout(() => {
161
- console.log('ffmpeg stream to image convesion timed out.');
162
- safeKillFFmpeg(cp);
163
- }, 10000);
164
- const [exitCode] = await once(cp, 'exit');
165
- clearTimeout(to);
166
- if (exitCode)
167
- throw new Error(`ffmpeg stream to image convesion failed with exit code: ${exitCode}`);
168
- return Buffer.concat(buffers);
169
- }
170
- });
171
149
  }
172
150
 
173
- async addConverter(converter: BufferConverter): Promise<void> {
151
+ async addConverter(converter: IdBufferConverter): Promise<void> {
152
+ converter.id = getExtraId(this.extraConverters.length);
174
153
  this.extraConverters.push(converter);
175
154
  }
176
155
 
@@ -219,10 +198,10 @@ export abstract class MediaManagerBase implements MediaManager {
219
198
  return ret;
220
199
  }
221
200
 
222
- getConverters(): BufferConverter[] {
201
+ getConverters(): IdBufferConverter[] {
223
202
  const converters = Object.entries(this.getSystemState())
224
203
  .filter(([id, state]) => state[ScryptedInterfaceProperty.interfaces]?.value?.includes(ScryptedInterface.BufferConverter))
225
- .map(([id]) => this.getDeviceById<BufferConverter>(id));
204
+ .map(([id]) => this.getDeviceById<IdBufferConverter>(id));
226
205
 
227
206
  // builtins should be after system converters. these should not be overriden by system,
228
207
  // as it could cause system instability with misconfiguration.
@@ -236,7 +215,7 @@ export abstract class MediaManagerBase implements MediaManager {
236
215
 
237
216
  ensureMediaObjectRemote(mediaObject: string | MediaObject): MediaObjectRemote {
238
217
  if (typeof mediaObject === 'string') {
239
- const mime = mimeType.lookup(mediaObject);
218
+ const mime = mimeType.getType(mediaObject);
240
219
  return this.createMediaObjectRemote(mediaObject, mime);
241
220
  }
242
221
  return mediaObject as MediaObjectRemote;
@@ -327,7 +306,7 @@ export abstract class MediaManagerBase implements MediaManager {
327
306
  return this.createMediaObjectRemote(data, mimeType, options);
328
307
  }
329
308
 
330
- async convert(converters: BufferConverter[], mediaObject: MediaObjectRemote, toMimeType: string): Promise<{ data: Buffer | string | any, mimeType: string }> {
309
+ async convert(converters: IdBufferConverter[], mediaObject: MediaObjectRemote, toMimeType: string): Promise<{ data: Buffer | string | any, mimeType: string }> {
331
310
  // console.log('converting', mediaObject.mimeType, toMimeType);
332
311
  const mediaMime = new MimeType(mediaObject.mimeType);
333
312
  const outputMime = new MimeType(toMimeType);
@@ -344,13 +323,9 @@ export abstract class MediaManagerBase implements MediaManager {
344
323
  sourceId = this.getPluginDeviceId();
345
324
  const console = this.getMixinConsole(sourceId, undefined);
346
325
 
347
- const converterIds = new Map<BufferConverter, string>();
348
- const converterReverseids = new Map<string, BufferConverter>();
349
- let id = 0;
350
- for (const converter of converters) {
351
- const cid = (id++).toString();
352
- converterIds.set(converter, cid);
353
- converterReverseids.set(cid, converter);
326
+ const converterMap = new Map<string, IdBufferConverter>();
327
+ for (const c of converters) {
328
+ converterMap.set(c.id, c);
354
329
  }
355
330
 
356
331
  const nodes: any = {};
@@ -361,33 +336,36 @@ export abstract class MediaManagerBase implements MediaManager {
361
336
  try {
362
337
  const inputMime = new MimeType(converter.fromMimeType);
363
338
  const convertedMime = new MimeType(converter.toMimeType);
364
- const targetId = converterIds.get(converter);
339
+ // catch all converters should be heavily weighted so as not to use them.
340
+ const inputWeight = parseFloat(inputMime.parameters.get('converter-weight')) || (inputMime.essence === '*/*' ? 1000 : 1);
341
+ // const convertedWeight = parseFloat(convertedMime.parameters.get('converter-weight')) || (convertedMime.essence === ScryptedMimeTypes.MediaObject ? 1000 : 1);
342
+ // const conversionWeight = inputWeight + convertedWeight;
343
+ const targetId = converter.id;
365
344
  const node: any = nodes[targetId] = {};
345
+
346
+ // edge matches
366
347
  for (const candidate of converters) {
367
348
  try {
368
349
  const candidateMime = new MimeType(candidate.fromMimeType);
369
350
  if (!mimeMatches(convertedMime, candidateMime))
370
351
  continue;
371
- const candidateId = converterIds.get(candidate);
372
- node[candidateId] = 1;
352
+ const outputWeight = parseFloat(candidateMime.parameters.get('converter-weight')) || (candidateMime.essence === '*/*' ? 1000 : 1);
353
+ const candidateId = candidate.id;
354
+ node[candidateId] = inputWeight + outputWeight;
373
355
  }
374
356
  catch (e) {
375
357
  console.warn('skipping converter due to error', e)
376
358
  }
377
359
  }
378
360
 
379
- // edge matches
361
+ // source matches
380
362
  if (mimeMatches(mediaMime, inputMime)) {
381
- const weight = parseFloat(inputMime.parameters.get('converter-weight'));
382
- // catch all converters should be heavily weighted so as not to use them.
383
- mediaNode[targetId] = weight || (inputMime.essence === '*/*' ? 1000 : 1);
363
+ mediaNode[targetId] = inputWeight;
384
364
  }
385
365
 
386
366
  // target output matches
387
367
  if (mimeMatches(outputMime, convertedMime) || converter.toMimeType === ScryptedMimeTypes.MediaObject) {
388
- const weight = parseFloat(inputMime.parameters.get('converter-weight'));
389
- // catch all converters should be heavily weighted so as not to use them.
390
- node['output'] = weight || (convertedMime.essence === ScryptedMimeTypes.MediaObject ? 1000 : 1);
368
+ node['output'] = inputWeight;
391
369
  }
392
370
  }
393
371
  catch (e) {
@@ -412,7 +390,7 @@ export abstract class MediaManagerBase implements MediaManager {
412
390
 
413
391
  while (route.length) {
414
392
  const node = route.shift();
415
- const converter = converterReverseids.get(node);
393
+ const converter = converterMap.get(node);
416
394
  const converterToMimeType = new MimeType(converter.toMimeType);
417
395
  const converterFromMimeType = new MimeType(converter.fromMimeType);
418
396
  const type = converterToMimeType.type === '*' ? valueMime.type : converterToMimeType.type;
@@ -449,6 +427,15 @@ export abstract class MediaManagerBase implements MediaManager {
449
427
  export class MediaManagerImpl extends MediaManagerBase {
450
428
  constructor(public systemManager: SystemManager, public deviceManager: DeviceManager) {
451
429
  super();
430
+
431
+ this.builtinConverters.push({
432
+ id: getBuiltinId(this.builtinConverters.length),
433
+ fromMimeType: ScryptedMimeTypes.ScryptedDeviceId,
434
+ toMimeType: ScryptedMimeTypes.ScryptedDevice,
435
+ convert: async (data, fromMimeType, toMimeType) => {
436
+ return this.getDeviceById(data.toString());
437
+ }
438
+ });
452
439
  }
453
440
 
454
441
  getSystemState(): { [id: string]: { [property: string]: SystemDeviceState; }; } {
@@ -11,8 +11,8 @@ export interface PluginAPI {
11
11
  setState(nativeId: ScryptedNativeId, key: string, value: any): Promise<void>;
12
12
  onDevicesChanged(deviceManifest: DeviceManifest): Promise<void>;
13
13
  onDeviceDiscovered(device: Device): Promise<string>;
14
- onDeviceEvent(nativeId: ScryptedNativeId, eventInterface: any, eventData?: any): Promise<void>;
15
- onMixinEvent(id: string, nativeId: ScryptedNativeId, eventInterface: any, eventData?: any): Promise<void>;
14
+ onDeviceEvent(nativeId: ScryptedNativeId, eventInterface: string, eventData?: any): Promise<void>;
15
+ onMixinEvent(id: string, nativeId: ScryptedNativeId, eventInterface: string, eventData?: any): Promise<void>;
16
16
  onDeviceRemoved(nativeId: string): Promise<void>;
17
17
  setStorage(nativeId: string, storage: {[key: string]: any}): Promise<void>;
18
18
 
@@ -89,8 +89,8 @@ export class PluginAPIProxy extends PluginAPIManagedListeners implements PluginA
89
89
  onDeviceEvent(nativeId: ScryptedNativeId, eventInterface: any, eventData?: any): Promise<void> {
90
90
  return this.api.onDeviceEvent(nativeId, eventInterface, eventData);
91
91
  }
92
- onMixinEvent(id: string, nativeId: ScryptedNativeId, eventInterface: any, eventData?: any): Promise<void> {
93
- return this.api.onMixinEvent(nativeId, eventInterface, eventData);
92
+ onMixinEvent(id: string, nativeId: ScryptedNativeId, eventInterface: string, eventData?: any): Promise<void> {
93
+ return this.api.onMixinEvent(id, nativeId, eventInterface, eventData);
94
94
  }
95
95
  onDeviceRemoved(nativeId: string): Promise<void> {
96
96
  return this.api.onDeviceRemoved(nativeId);
@@ -202,7 +202,9 @@ export class PluginDeviceProxyHandler implements PrimitiveProxyHandler<any>, Scr
202
202
  }
203
203
 
204
204
  return this.mixinTable[0].entry.then(entry => {
205
- this.scrypted.stateManager.setPluginDeviceState(pluginDevice, ScryptedInterfaceProperty.interfaces, PluginDeviceProxyHandler.sortInterfaces(entry.allInterfaces));
205
+ const changed = this.scrypted.stateManager.setPluginDeviceState(pluginDevice, ScryptedInterfaceProperty.interfaces, PluginDeviceProxyHandler.sortInterfaces(entry.allInterfaces));
206
+ if (changed)
207
+ this.scrypted.notifyPluginDeviceDescriptorChanged(pluginDevice);
206
208
  return pluginDevice;
207
209
  });
208
210
  }
@@ -216,15 +218,25 @@ export class PluginDeviceProxyHandler implements PrimitiveProxyHandler<any>, Scr
216
218
  let { allInterfaces } = await previousEntry;
217
219
  try {
218
220
  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[];
221
+ const isMixinProvider = mixinProvider?.interfaces?.includes(ScryptedInterface.MixinProvider);
222
+ const interfaces = isMixinProvider && await mixinProvider?.canMixin(type, allInterfaces) as any as ScryptedInterface[];
220
223
  if (!interfaces) {
221
224
  // this is not an error
222
225
  // do not advertise interfaces so it is skipped during
223
226
  // 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);
227
+ if (!mixinProvider || (isMixinProvider && !interfaces)) {
228
+ console.log(`Mixin provider ${mixinId} can no longer mixin ${this.id}. Removing.`, {
229
+ mixinProvider: !!mixinProvider,
230
+ interfaces,
231
+ });
232
+ const mixins: string[] = getState(pluginDevice, ScryptedInterfaceProperty.mixins) || [];
233
+ this.scrypted.stateManager.setPluginDeviceState(pluginDevice, ScryptedInterfaceProperty.mixins, mixins.filter(mid => mid !== mixinId));
234
+ this.scrypted.notifyPluginDeviceDescriptorChanged(pluginDevice);
235
+ this.scrypted.datastore.upsert(pluginDevice);
236
+ }
237
+ else {
238
+ console.log(`Mixin provider ${mixinId} can not mixin ${this.id}. It is no longer a MixinProvider. This may be temporary. Passing through.`);
239
+ }
228
240
  return {
229
241
  passthrough: true,
230
242
  allInterfaces,
@@ -353,12 +365,17 @@ export class PluginDeviceProxyHandler implements PrimitiveProxyHandler<any>, Scr
353
365
  for (const mixin of this.mixinTable) {
354
366
  const entry = await mixin.entry;
355
367
  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;
368
+ if (entry.interfaces.size) {
369
+ const pluginDevice = this.scrypted.findPluginDeviceById(mixin.mixinProviderId || this.id);
370
+ const plugin = this.scrypted.plugins[pluginDevice.pluginId];
371
+ let methods = new Set<string>(getInterfaceMethods(ScryptedInterfaceDescriptors, entry.interfaces))
372
+ if (plugin.api.descriptors)
373
+ methods = new Set<string>([...methods, ...getInterfaceMethods(plugin.api.descriptors, entry.interfaces)]);
374
+ entry.methods = methods;
375
+ }
376
+ else {
377
+ entry.methods = new Set();
378
+ }
362
379
  }
363
380
  if (entry.methods.has(method)) {
364
381
  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
 
@@ -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...');
@@ -9,18 +9,24 @@ import { install as installSourceMapSupport } from 'source-map-support';
9
9
  import { PassThrough } from 'stream';
10
10
  import { RpcMessage, RpcPeer } from '../rpc';
11
11
  import { MediaManagerImpl } from './media';
12
- import { PluginAPI, PluginRemoteLoadZipOptions } from './plugin-api';
12
+ import { PluginAPI, PluginAPIProxy, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
13
13
  import { installOptionalDependencies } from './plugin-npm-dependencies';
14
- import { attachPluginRemote, PluginReader, setupPluginRemote } from './plugin-remote';
14
+ import { attachPluginRemote, DeviceManagerImpl, PluginReader, setupPluginRemote } from './plugin-remote';
15
15
  import { createREPLServer } from './plugin-repl';
16
16
  import { NodeThreadWorker } from './runtime/node-thread-worker';
17
17
  const { link } = require('linkfs');
18
18
 
19
+ interface PluginStats {
20
+ type: 'stats',
21
+ cpu: NodeJS.CpuUsage;
22
+ memoryUsage: NodeJS.MemoryUsage;
23
+ }
24
+
19
25
  export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessage, reject?: (e: Error) => void, serializationContext?: any) => void) {
20
26
  const peer = new RpcPeer('unknown', 'host', peerSend);
21
27
 
22
28
  let systemManager: SystemManager;
23
- let deviceManager: DeviceManager;
29
+ let deviceManager: DeviceManagerImpl;
24
30
  let api: PluginAPI;
25
31
 
26
32
  const getConsole = (hook: (stdout: PassThrough, stderr: PassThrough) => Promise<void>,
@@ -126,12 +132,6 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
126
132
  if (!mixinId) {
127
133
  return;
128
134
  }
129
- // todo: fix this. a mixin provider can mixin another device to make it a mixin provider itself.
130
- // so the mixin id in the mixin table will be incorrect.
131
- // there's no easy way to fix this from the remote.
132
- // if (!systemManager.getDeviceById(mixinId).mixins.includes(idForNativeId(nativeId))) {
133
- // return;
134
- // }
135
135
  const reconnect = () => {
136
136
  stdout.removeAllListeners();
137
137
  stderr.removeAllListeners();
@@ -178,15 +178,35 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
178
178
  return ret;
179
179
  }
180
180
 
181
- peer.getParam('updateStats').then((updateStats: (stats: any) => void) => {
182
- 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) => {
183
186
  setInterval(() => {
184
- const cpuUsage = process.cpuUsage(lastCpuUsage);
185
- 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
+
186
206
  updateStats({
187
207
  type: 'stats',
188
208
  cpu: cpuUsage,
189
- memoryUsage: process.memoryUsage(),
209
+ memoryUsage,
190
210
  });
191
211
  }, 10000);
192
212
  });
@@ -316,35 +336,75 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
316
336
  pluginReader = undefined;
317
337
  const script = main.toString();
318
338
 
319
- scrypted.fork = async () => {
339
+
340
+ const forks = new Set<PluginRemote>();
341
+
342
+ scrypted.fork = () => {
320
343
  const ntw = new NodeThreadWorker(pluginId, {
321
344
  env: process.env,
322
345
  pluginDebug: undefined,
323
346
  });
324
- const threadPeer = new RpcPeer('main', 'thread', (message, reject) => ntw.send(message, reject));
325
- threadPeer.params.updateStats = (stats: any) => {
326
- // todo: merge.
327
- // this.stats = stats;
328
- }
329
- ntw.setupRpcPeer(threadPeer);
330
347
 
331
- const remote = await setupPluginRemote(threadPeer, api, pluginId, () => systemManager.getSystemState());
332
- const forkOptions = Object.assign({}, zipOptions);
333
- forkOptions.fork = true;
334
- return remote.loadZip(packageJson, zipData, forkOptions)
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
+ }
335
394
  }
336
395
 
337
396
  try {
338
397
  peer.evalLocal(script, zipOptions?.filename || '/plugin/main.nodejs.js', params);
339
- pluginConsole?.log('plugin successfully loaded');
340
398
 
341
399
  if (zipOptions?.fork) {
400
+ pluginConsole?.log('plugin forked');
342
401
  const fork = exports.fork;
343
- const ret = await fork();
344
- ret[RpcPeer.PROPERTY_JSON_DISABLE_SERIALIZATION] = true;
345
- return ret;
402
+ const forked = await fork();
403
+ forked[RpcPeer.PROPERTY_JSON_DISABLE_SERIALIZATION] = true;
404
+ return forked;
346
405
  }
347
406
 
407
+ pluginConsole?.log('plugin loaded');
348
408
  let pluginInstance = exports.default;
349
409
  // support exporting a plugin class, plugin main function,
350
410
  // or a plugin instance
@@ -365,7 +425,7 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
365
425
  }
366
426
  }).then(scrypted => {
367
427
  systemManager = scrypted.systemManager;
368
- deviceManager = scrypted.deviceManager;
428
+ deviceManager = scrypted.deviceManager as DeviceManagerImpl;
369
429
  });
370
430
 
371
431
  return peer;
@@ -107,6 +107,10 @@ class DeviceStateProxyHandler implements ProxyHandler<any> {
107
107
  get?(target: any, p: PropertyKey, receiver: any) {
108
108
  if (p === 'id')
109
109
  return this.id;
110
+ if (p === RpcPeer.PROPERTY_PROXY_PROPERTIES)
111
+ return { id: this.id }
112
+ if (p === 'setState')
113
+ return this.setState;
110
114
  return this.deviceManager.systemManager.state[this.id][p as string]?.value;
111
115
  }
112
116
 
@@ -128,7 +132,7 @@ interface DeviceManagerDevice {
128
132
  storage: { [key: string]: any };
129
133
  }
130
134
 
131
- class DeviceManagerImpl implements DeviceManager {
135
+ export class DeviceManagerImpl implements DeviceManager {
132
136
  api: PluginAPI;
133
137
  nativeIds = new Map<string, DeviceManagerDevice>();
134
138
  deviceStorage = new Map<string, StorageImpl>();
@@ -153,6 +157,11 @@ class DeviceManagerImpl implements DeviceManager {
153
157
  return new Proxy(handler, handler);
154
158
  }
155
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
+
156
165
  getDeviceStorage(nativeId?: any): StorageImpl {
157
166
  let ret = this.deviceStorage.get(nativeId);
158
167
  if (!ret) {
@@ -297,7 +306,7 @@ export async function setupPluginRemote(peer: RpcPeer, api: PluginAPI, pluginId:
297
306
  if (!peer.constructorSerializerMap.get(Buffer))
298
307
  peer.addSerializer(Buffer, 'Buffer', new BufferSerializer());
299
308
  const getRemote = await peer.getParam('getRemote');
300
- const remote = await getRemote(api, pluginId);
309
+ const remote = await getRemote(api, pluginId) as PluginRemote;
301
310
 
302
311
  await remote.setSystemState(getSystemState());
303
312
  api.listen((id, eventDetails, eventData) => {
@@ -337,7 +346,7 @@ export interface WebSocketCustomHandler {
337
346
  export type PluginReader = (name: string) => Buffer;
338
347
 
339
348
  export interface PluginRemoteAttachOptions {
340
- createMediaManager?: (systemManager: SystemManager, deviceManager: DeviceManager) => Promise<MediaManager>;
349
+ createMediaManager?: (systemManager: SystemManager, deviceManager: DeviceManagerImpl) => Promise<MediaManager>;
341
350
  getServicePort?: (name: string, ...args: any[]) => Promise<number>;
342
351
  getDeviceConsole?: (nativeId?: ScryptedNativeId) => Console;
343
352
  getPluginConsole?: () => Console;
@@ -381,6 +390,7 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
381
390
  mediaManager,
382
391
  log,
383
392
  pluginHostAPI: api,
393
+ pluginRemoteAPI: undefined,
384
394
  }
385
395
 
386
396
  delete peer.params.getRemote;
@@ -402,9 +412,8 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
402
412
  'setNativeId',
403
413
  ],
404
414
  getServicePort,
405
- createDeviceState(id: string, setState: (property: string, value: any) => Promise<void>) {
406
- const handler = new DeviceStateProxyHandler(deviceManager, id, setState);
407
- return new Proxy(handler, handler);
415
+ async createDeviceState(id: string, setState: (property: string, value: any) => Promise<void>) {
416
+ return deviceManager.createDeviceState(id, setState);
408
417
  },
409
418
 
410
419
  async ioEvent(id: string, event: string, message?: any) {
@@ -507,6 +516,8 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
507
516
  },
508
517
  }
509
518
 
519
+ ret.pluginRemoteAPI = remote;
520
+
510
521
  return remote;
511
522
  }
512
523
 
@@ -64,6 +64,6 @@ export class NodeForkWorker extends ChildProcessWorker {
64
64
  }
65
65
 
66
66
  get pid() {
67
- return this.worker.pid;
67
+ return this.worker?.pid;
68
68
  }
69
69
  }