@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.
- package/dist/event-registry.js +3 -4
- package/dist/event-registry.js.map +1 -1
- package/dist/plugin/media.js +51 -63
- package/dist/plugin/media.js.map +1 -1
- package/dist/plugin/plugin-api.js +1 -1
- package/dist/plugin/plugin-api.js.map +1 -1
- package/dist/plugin/plugin-device.js +29 -12
- package/dist/plugin/plugin-device.js.map +1 -1
- package/dist/plugin/plugin-host-api.js.map +1 -1
- package/dist/plugin/plugin-host.js +5 -2
- package/dist/plugin/plugin-host.js.map +1 -1
- package/dist/plugin/plugin-remote-worker.js +66 -24
- package/dist/plugin/plugin-remote-worker.js.map +1 -1
- package/dist/plugin/plugin-remote.js +14 -4
- package/dist/plugin/plugin-remote.js.map +1 -1
- package/dist/plugin/runtime/node-fork-worker.js +1 -1
- package/dist/plugin/runtime/node-fork-worker.js.map +1 -1
- package/dist/plugin/system.js +1 -1
- package/dist/plugin/system.js.map +1 -1
- package/dist/rpc.js +2 -2
- package/dist/rpc.js.map +1 -1
- package/dist/runtime.js +11 -16
- package/dist/runtime.js.map +1 -1
- package/dist/scrypted-server-main.js +8 -4
- package/dist/scrypted-server-main.js.map +1 -1
- package/dist/server-settings.js +5 -1
- package/dist/server-settings.js.map +1 -1
- package/dist/services/plugin.js +1 -0
- package/dist/services/plugin.js.map +1 -1
- package/dist/state.js +3 -2
- package/dist/state.js.map +1 -1
- package/package.json +6 -12
- package/scripts/github-workflow-publish-docker.sh +2 -0
- package/scripts/print-package-json-version.js +2 -0
- package/src/event-registry.ts +3 -4
- package/src/plugin/media.ts +69 -82
- package/src/plugin/plugin-api.ts +4 -4
- package/src/plugin/plugin-device.ts +29 -12
- package/src/plugin/plugin-host-api.ts +1 -1
- package/src/plugin/plugin-host.ts +0 -1
- package/src/plugin/plugin-remote-worker.ts +90 -30
- package/src/plugin/plugin-remote.ts +17 -6
- package/src/plugin/runtime/node-fork-worker.ts +1 -1
- package/src/plugin/system.ts +1 -1
- package/src/rpc.ts +2 -1
- package/src/runtime.ts +6 -16
- package/src/scrypted-server-main.ts +8 -4
- package/src/services/plugin.ts +1 -0
- package/src/state.ts +3 -2
package/src/plugin/media.ts
CHANGED
@@ -1,7 +1,5 @@
|
|
1
|
-
import { BufferConverter,
|
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:
|
34
|
-
extraConverters:
|
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
|
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.
|
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:
|
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():
|
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<
|
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.
|
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:
|
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
|
348
|
-
const
|
349
|
-
|
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
|
-
|
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
|
372
|
-
|
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
|
-
//
|
361
|
+
// source matches
|
380
362
|
if (mimeMatches(mediaMime, inputMime)) {
|
381
|
-
|
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
|
-
|
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 =
|
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; }; } {
|
package/src/plugin/plugin-api.ts
CHANGED
@@ -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:
|
15
|
-
onMixinEvent(id: string, nativeId: ScryptedNativeId, eventInterface:
|
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:
|
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
|
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
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
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
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
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:
|
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:
|
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
|
-
|
182
|
-
|
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(
|
185
|
-
|
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
|
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
|
-
|
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
|
332
|
-
|
333
|
-
|
334
|
-
|
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
|
344
|
-
|
345
|
-
return
|
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:
|
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
|
-
|
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
|
|