@scrypted/server 0.0.165 → 0.0.168
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/bin/scrypted-serve +0 -0
- package/dist/media-helpers.js +5 -1
- package/dist/media-helpers.js.map +1 -1
- package/dist/plugin/media.js +55 -26
- package/dist/plugin/media.js.map +1 -1
- package/dist/plugin/plugin-host-api.js +2 -0
- package/dist/plugin/plugin-host-api.js.map +1 -1
- package/dist/plugin/plugin-host.js +14 -13
- package/dist/plugin/plugin-host.js.map +1 -1
- package/dist/plugin/plugin-http.js +16 -9
- package/dist/plugin/plugin-http.js.map +1 -1
- package/dist/plugin/plugin-remote-worker.js +8 -7
- package/dist/plugin/plugin-remote-worker.js.map +1 -1
- package/dist/plugin/plugin-remote.js +19 -7
- package/dist/plugin/plugin-remote.js.map +1 -1
- package/dist/plugin/plugin-state-check.js +18 -0
- package/dist/plugin/plugin-state-check.js.map +1 -0
- package/dist/plugin/system.js +0 -4
- package/dist/plugin/system.js.map +1 -1
- package/dist/runtime.js +23 -2
- package/dist/runtime.js.map +1 -1
- package/dist/scrypted-server-main.js +0 -5
- package/dist/scrypted-server-main.js.map +1 -1
- package/dist/services/plugin.js +5 -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 +2 -2
- package/python/plugin-remote.py +6 -6
- package/python/plugin-repl.py +9 -0
- package/src/media-helpers.ts +5 -1
- package/src/plugin/media.ts +68 -33
- package/src/plugin/plugin-host-api.ts +5 -2
- package/src/plugin/plugin-host.ts +20 -19
- package/src/plugin/plugin-http.ts +16 -9
- package/src/plugin/plugin-remote-worker.ts +10 -9
- package/src/plugin/plugin-remote.ts +21 -8
- package/src/plugin/plugin-state-check.ts +14 -0
- package/src/plugin/system.ts +0 -4
- package/src/runtime.ts +27 -2
- package/src/scrypted-server-main.ts +0 -5
- package/src/services/plugin.ts +5 -0
- package/src/state.ts +3 -2
package/src/media-helpers.ts
CHANGED
@@ -11,7 +11,11 @@ export function safeKillFFmpeg(cp: ChildProcess) {
|
|
11
11
|
if (!cp)
|
12
12
|
return;
|
13
13
|
// this will allow ffmpeg to send rtsp TEARDOWN etc
|
14
|
-
|
14
|
+
try {
|
15
|
+
cp.stdin.write('q\n');
|
16
|
+
}
|
17
|
+
catch (e) {
|
18
|
+
}
|
15
19
|
setTimeout(() => {
|
16
20
|
cp.kill();
|
17
21
|
setTimeout(() => {
|
package/src/plugin/media.ts
CHANGED
@@ -1,19 +1,19 @@
|
|
1
|
-
import {
|
2
|
-
import {
|
3
|
-
import
|
1
|
+
import { getInstalledFfmpeg } from '@scrypted/ffmpeg';
|
2
|
+
import { BufferConverter, BufferConvertorOptions, DeviceManager, FFmpegInput, MediaManager, MediaObject, MediaObjectOptions, MediaStreamUrl, ScryptedInterface, ScryptedInterfaceProperty, ScryptedMimeTypes, ScryptedNativeId, SystemDeviceState, SystemManager } from "@scrypted/types";
|
3
|
+
import axios from 'axios';
|
4
4
|
import child_process from 'child_process';
|
5
5
|
import { once } from 'events';
|
6
6
|
import fs from 'fs';
|
7
|
-
import tmp from 'tmp';
|
8
|
-
import os from 'os';
|
9
|
-
import { getInstalledFfmpeg } from '@scrypted/ffmpeg'
|
10
|
-
import Graph from 'node-dijkstra';
|
11
|
-
import MimeType from 'whatwg-mimetype';
|
12
|
-
import axios from 'axios';
|
13
7
|
import https from 'https';
|
14
|
-
import
|
8
|
+
import mimeType from 'mime';
|
15
9
|
import mkdirp from "mkdirp";
|
10
|
+
import Graph from 'node-dijkstra';
|
11
|
+
import os from 'os';
|
16
12
|
import path from 'path';
|
13
|
+
import rimraf from "rimraf";
|
14
|
+
import tmp from 'tmp';
|
15
|
+
import MimeType from 'whatwg-mimetype';
|
16
|
+
import { MediaObjectRemote } from "./plugin-api";
|
17
17
|
|
18
18
|
function typeMatches(target: string, candidate: string): boolean {
|
19
19
|
// candidate will accept anything
|
@@ -33,7 +33,7 @@ const httpsAgent = new https.Agent({
|
|
33
33
|
export abstract class MediaManagerBase implements MediaManager {
|
34
34
|
builtinConverters: BufferConverter[] = [];
|
35
35
|
|
36
|
-
constructor(
|
36
|
+
constructor() {
|
37
37
|
for (const h of ['http', 'https']) {
|
38
38
|
this.builtinConverters.push({
|
39
39
|
fromMimeType: ScryptedMimeTypes.SchemePrefix + h,
|
@@ -56,7 +56,7 @@ export abstract class MediaManagerBase implements MediaManager {
|
|
56
56
|
convert: async (data, fromMimeType, toMimeType) => {
|
57
57
|
const filename = data.toString();
|
58
58
|
const ab = await fs.promises.readFile(filename);
|
59
|
-
const mt =
|
59
|
+
const mt = mimeType.lookup(data.toString());
|
60
60
|
const mo = this.createMediaObject(ab, mt);
|
61
61
|
return mo;
|
62
62
|
}
|
@@ -67,7 +67,7 @@ export abstract class MediaManagerBase implements MediaManager {
|
|
67
67
|
toMimeType: ScryptedMimeTypes.FFmpegInput,
|
68
68
|
async convert(data, fromMimeType): Promise<Buffer> {
|
69
69
|
const url = data.toString();
|
70
|
-
const args:
|
70
|
+
const args: FFmpegInput = {
|
71
71
|
url,
|
72
72
|
inputArguments: [
|
73
73
|
'-i', url,
|
@@ -96,13 +96,13 @@ export abstract class MediaManagerBase implements MediaManager {
|
|
96
96
|
'-i', mediaUrl.url,
|
97
97
|
];
|
98
98
|
|
99
|
-
if (mediaUrl.url.startsWith('rtsp
|
99
|
+
if (mediaUrl.url.startsWith('rtsp')) {
|
100
100
|
inputArguments.unshift(
|
101
101
|
"-rtsp_transport", "tcp",
|
102
102
|
);
|
103
103
|
}
|
104
104
|
|
105
|
-
const ret:
|
105
|
+
const ret: FFmpegInput = Object.assign({
|
106
106
|
inputArguments,
|
107
107
|
}, mediaUrl);
|
108
108
|
|
@@ -121,8 +121,10 @@ export abstract class MediaManagerBase implements MediaManager {
|
|
121
121
|
this.builtinConverters.push({
|
122
122
|
fromMimeType: ScryptedMimeTypes.FFmpegInput,
|
123
123
|
toMimeType: 'image/jpeg',
|
124
|
-
convert: async (data, fromMimeType: string): Promise<Buffer> => {
|
125
|
-
const
|
124
|
+
convert: async (data, fromMimeType: string, toMimeType: string, options?: BufferConvertorOptions): Promise<Buffer> => {
|
125
|
+
const console = this.getMixinConsole(options?.sourceId, undefined);
|
126
|
+
|
127
|
+
const ffInput: FFmpegInput = JSON.parse(data.toString());
|
126
128
|
|
127
129
|
const args = [
|
128
130
|
'-hide_banner',
|
@@ -161,6 +163,8 @@ export abstract class MediaManagerBase implements MediaManager {
|
|
161
163
|
|
162
164
|
abstract getSystemState(): { [id: string]: { [property: string]: SystemDeviceState } };
|
163
165
|
abstract getDeviceById<T>(id: string): T;
|
166
|
+
abstract getPluginDeviceId(): string;
|
167
|
+
abstract getMixinConsole(mixinId: string, nativeId: ScryptedNativeId): Console;
|
164
168
|
|
165
169
|
async getFFmpegPath(): Promise<string> {
|
166
170
|
// try to get the ffmpeg path as a value of another variable
|
@@ -238,11 +242,7 @@ export abstract class MediaManagerBase implements MediaManager {
|
|
238
242
|
return url.data.toString();
|
239
243
|
}
|
240
244
|
|
241
|
-
|
242
|
-
return this.createMediaObjectRemote(Buffer.from(JSON.stringify(ffMpegInput)), ScryptedMimeTypes.FFmpegInput);
|
243
|
-
}
|
244
|
-
|
245
|
-
createMediaObjectRemote(data: any | Buffer | Promise<string | Buffer>, mimeType: string): MediaObjectRemote {
|
245
|
+
createMediaObjectRemote(data: any | Buffer | Promise<string | Buffer>, mimeType: string, options?: MediaObjectOptions): MediaObjectRemote {
|
246
246
|
if (typeof data === 'string')
|
247
247
|
throw new Error('string is not a valid type. if you intended to send a url, use createMediaObjectFromUrl.');
|
248
248
|
if (!mimeType)
|
@@ -253,12 +253,15 @@ export abstract class MediaManagerBase implements MediaManager {
|
|
253
253
|
if (data.constructor.name !== Buffer.name)
|
254
254
|
data = Buffer.from(JSON.stringify(data));
|
255
255
|
|
256
|
+
const sourceId = typeof options?.sourceId === 'string' ? options?.sourceId : this.getPluginDeviceId();
|
256
257
|
class MediaObjectImpl implements MediaObjectRemote {
|
257
258
|
__proxy_props = {
|
258
259
|
mimeType,
|
260
|
+
sourceId,
|
259
261
|
}
|
260
262
|
|
261
263
|
mimeType = mimeType;
|
264
|
+
sourceId = sourceId;
|
262
265
|
async getData(): Promise<Buffer | string> {
|
263
266
|
return Promise.resolve(data);
|
264
267
|
}
|
@@ -266,21 +269,24 @@ export abstract class MediaManagerBase implements MediaManager {
|
|
266
269
|
return new MediaObjectImpl();
|
267
270
|
}
|
268
271
|
|
269
|
-
async
|
270
|
-
return this.createMediaObjectRemote(
|
272
|
+
async createFFmpegMediaObject(ffMpegInput: FFmpegInput, options?: MediaObjectOptions): Promise<MediaObject> {
|
273
|
+
return this.createMediaObjectRemote(Buffer.from(JSON.stringify(ffMpegInput)), ScryptedMimeTypes.FFmpegInput, options);
|
271
274
|
}
|
272
275
|
|
273
|
-
async createMediaObjectFromUrl(data: string): Promise<MediaObject> {
|
276
|
+
async createMediaObjectFromUrl(data: string, options?: MediaObjectOptions): Promise<MediaObject> {
|
274
277
|
const url = new URL(data);
|
275
278
|
const scheme = url.protocol.slice(0, -1);
|
276
279
|
const mimeType = ScryptedMimeTypes.SchemePrefix + scheme;
|
277
280
|
|
281
|
+
const sourceId = typeof options?.sourceId === 'string' ? options?.sourceId : this.getPluginDeviceId();
|
278
282
|
class MediaObjectImpl implements MediaObjectRemote {
|
279
283
|
__proxy_props = {
|
280
284
|
mimeType,
|
285
|
+
sourceId,
|
281
286
|
}
|
282
287
|
|
283
288
|
mimeType = mimeType;
|
289
|
+
sourceId = sourceId;
|
284
290
|
async getData(): Promise<Buffer | string> {
|
285
291
|
return Promise.resolve(data);
|
286
292
|
}
|
@@ -288,6 +294,10 @@ export abstract class MediaManagerBase implements MediaManager {
|
|
288
294
|
return new MediaObjectImpl();
|
289
295
|
}
|
290
296
|
|
297
|
+
async createMediaObject(data: any, mimeType: string, options?: MediaObjectOptions): Promise<MediaObject> {
|
298
|
+
return this.createMediaObjectRemote(data, mimeType, options);
|
299
|
+
}
|
300
|
+
|
291
301
|
async convert(converters: BufferConverter[], mediaObject: MediaObjectRemote, toMimeType: string): Promise<{ data: Buffer | string | any, mimeType: string }> {
|
292
302
|
// console.log('converting', mediaObject.mimeType, toMimeType);
|
293
303
|
const mediaMime = new MimeType(mediaObject.mimeType);
|
@@ -300,6 +310,11 @@ export abstract class MediaManagerBase implements MediaManager {
|
|
300
310
|
}
|
301
311
|
}
|
302
312
|
|
313
|
+
let sourceId = mediaObject?.sourceId;
|
314
|
+
if (typeof sourceId !== 'string')
|
315
|
+
sourceId = this.getPluginDeviceId();
|
316
|
+
const console = this.getMixinConsole(sourceId, undefined);
|
317
|
+
|
303
318
|
const converterIds = new Map<BufferConverter, string>();
|
304
319
|
const converterReverseids = new Map<string, BufferConverter>();
|
305
320
|
let id = 0;
|
@@ -363,6 +378,7 @@ export abstract class MediaManagerBase implements MediaManager {
|
|
363
378
|
route.splice(route.length - 1);
|
364
379
|
let value = await mediaObject.getData();
|
365
380
|
let valueMime = new MimeType(mediaObject.mimeType);
|
381
|
+
|
366
382
|
for (const node of route) {
|
367
383
|
const converter = converterReverseids.get(node);
|
368
384
|
const converterToMimeType = new MimeType(converter.toMimeType);
|
@@ -372,7 +388,7 @@ export abstract class MediaManagerBase implements MediaManager {
|
|
372
388
|
const targetMimeType = `${type}/${subtype}`;
|
373
389
|
|
374
390
|
if (converter.toMimeType === ScryptedMimeTypes.MediaObject) {
|
375
|
-
const mo = await converter.convert(value, valueMime.essence, toMimeType) as MediaObject;
|
391
|
+
const mo = await converter.convert(value, valueMime.essence, toMimeType, { sourceId }) as MediaObject;
|
376
392
|
const found = await this.convertMediaObjectToBuffer(mo, toMimeType);
|
377
393
|
return {
|
378
394
|
data: found,
|
@@ -380,7 +396,7 @@ export abstract class MediaManagerBase implements MediaManager {
|
|
380
396
|
};
|
381
397
|
}
|
382
398
|
|
383
|
-
value = await converter.convert(value, valueMime.essence, targetMimeType) as string | Buffer;
|
399
|
+
value = await converter.convert(value, valueMime.essence, targetMimeType, { sourceId }) as string | Buffer;
|
384
400
|
valueMime = new MimeType(targetMimeType);
|
385
401
|
}
|
386
402
|
|
@@ -392,8 +408,8 @@ export abstract class MediaManagerBase implements MediaManager {
|
|
392
408
|
}
|
393
409
|
|
394
410
|
export class MediaManagerImpl extends MediaManagerBase {
|
395
|
-
constructor(public systemManager: SystemManager,
|
396
|
-
super(
|
411
|
+
constructor(public systemManager: SystemManager, public deviceManager: DeviceManager) {
|
412
|
+
super();
|
397
413
|
}
|
398
414
|
|
399
415
|
getSystemState(): { [id: string]: { [property: string]: SystemDeviceState; }; } {
|
@@ -403,16 +419,35 @@ export class MediaManagerImpl extends MediaManagerBase {
|
|
403
419
|
getDeviceById<T>(id: string): T {
|
404
420
|
return this.systemManager.getDeviceById<T>(id);
|
405
421
|
}
|
422
|
+
|
423
|
+
getPluginDeviceId(): string {
|
424
|
+
return this.deviceManager.getDeviceState().id;
|
425
|
+
}
|
426
|
+
|
427
|
+
getMixinConsole(mixinId: string, nativeId: string): Console {
|
428
|
+
if (typeof mixinId !== 'string')
|
429
|
+
return this.deviceManager.getDeviceConsole(nativeId);
|
430
|
+
return this.deviceManager.getMixinConsole(mixinId, nativeId);
|
431
|
+
}
|
406
432
|
}
|
407
433
|
|
408
434
|
export class MediaManagerHostImpl extends MediaManagerBase {
|
409
|
-
constructor(public
|
410
|
-
public
|
411
|
-
console: Console
|
412
|
-
|
435
|
+
constructor(public pluginDeviceId: string,
|
436
|
+
public systemState: { [id: string]: { [property: string]: SystemDeviceState } },
|
437
|
+
public console: Console,
|
438
|
+
public getDeviceById: (id: string) => any) {
|
439
|
+
super();
|
413
440
|
}
|
414
441
|
|
415
442
|
getSystemState(): { [id: string]: { [property: string]: SystemDeviceState; }; } {
|
416
443
|
return this.systemState;
|
417
444
|
}
|
445
|
+
|
446
|
+
getPluginDeviceId(): string {
|
447
|
+
return this.pluginDeviceId;
|
448
|
+
}
|
449
|
+
|
450
|
+
getMixinConsole(mixinId: string, nativeId: string): Console {
|
451
|
+
return this.console;
|
452
|
+
}
|
418
453
|
}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { ScryptedNativeId, ScryptedDevice, Device, DeviceManifest, EventDetails, EventListenerOptions, EventListenerRegister, ScryptedInterfaceProperty, MediaManager, HttpRequest } from '@scrypted/types'
|
1
|
+
import { ScryptedNativeId, ScryptedDevice, Device, DeviceManifest, EventDetails, EventListenerOptions, EventListenerRegister, ScryptedInterfaceProperty, MediaManager, HttpRequest, ScryptedInterface } from '@scrypted/types'
|
2
2
|
import { ScryptedRuntime } from '../runtime';
|
3
3
|
import { Plugin } from '../db-types';
|
4
4
|
import { PluginAPI, PluginAPIManagedListeners } from './plugin-api';
|
@@ -7,6 +7,8 @@ import { getState } from '../state';
|
|
7
7
|
import { PluginHost } from './plugin-host';
|
8
8
|
import debounce from 'lodash/debounce';
|
9
9
|
import { RpcPeer } from '../rpc';
|
10
|
+
import { propertyInterfaces } from './descriptor';
|
11
|
+
import { checkProperty } from './plugin-state-check';
|
10
12
|
|
11
13
|
export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAPI {
|
12
14
|
pluginId: string;
|
@@ -42,7 +44,7 @@ export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAP
|
|
42
44
|
|
43
45
|
// do we care about mixin validation here?
|
44
46
|
// maybe to prevent/notify errant dangling events?
|
45
|
-
async onMixinEvent(id: string, nativeIdOrMixinDevice: ScryptedNativeId|any, eventInterface: any, eventData?: any) {
|
47
|
+
async onMixinEvent(id: string, nativeIdOrMixinDevice: ScryptedNativeId | any, eventInterface: any, eventData?: any) {
|
46
48
|
// nativeId code path has been deprecated in favor of mixin object 12/10/2021
|
47
49
|
const device = this.scrypted.findPluginDeviceById(id);
|
48
50
|
|
@@ -111,6 +113,7 @@ export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAP
|
|
111
113
|
}
|
112
114
|
|
113
115
|
async setState(nativeId: ScryptedNativeId, key: string, value: any) {
|
116
|
+
checkProperty(key, value);
|
114
117
|
this.scrypted.stateManager.setPluginState(this.pluginId, nativeId, key, value);
|
115
118
|
}
|
116
119
|
|
@@ -1,29 +1,29 @@
|
|
1
|
-
import {
|
1
|
+
import { Device, EngineIOHandler } from '@scrypted/types';
|
2
2
|
import AdmZip from 'adm-zip';
|
3
|
-
import
|
4
|
-
import { ScryptedRuntime } from '../runtime';
|
5
|
-
import { Plugin } from '../db-types';
|
3
|
+
import crypto from 'crypto';
|
6
4
|
import io, { Socket } from 'engine.io';
|
7
|
-
import
|
8
|
-
import
|
9
|
-
import
|
10
|
-
import
|
5
|
+
import fs from 'fs';
|
6
|
+
import mkdirp from 'mkdirp';
|
7
|
+
import path from 'path';
|
8
|
+
import rimraf from 'rimraf';
|
11
9
|
import WebSocket from 'ws';
|
10
|
+
import { Plugin } from '../db-types';
|
11
|
+
import { Logger } from '../logger';
|
12
|
+
import { RpcPeer } from '../rpc';
|
13
|
+
import { ScryptedRuntime } from '../runtime';
|
12
14
|
import { sleep } from '../sleep';
|
13
|
-
import {
|
14
|
-
import
|
15
|
-
import { PluginDebug } from './plugin-debug';
|
16
|
-
import { ensurePluginVolume, getScryptedVolume } from './plugin-volume';
|
15
|
+
import { MediaManagerHostImpl } from './media';
|
16
|
+
import { PluginAPIProxy, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
|
17
17
|
import { ConsoleServer, createConsoleServer } from './plugin-console';
|
18
|
+
import { PluginDebug } from './plugin-debug';
|
19
|
+
import { PluginHostAPI } from './plugin-host-api';
|
18
20
|
import { LazyRemote } from './plugin-lazy-remote';
|
19
|
-
import
|
20
|
-
import
|
21
|
-
import mkdirp from 'mkdirp';
|
22
|
-
import rimraf from 'rimraf';
|
23
|
-
import { RuntimeWorker } from './runtime/runtime-worker';
|
24
|
-
import { PythonRuntimeWorker } from './runtime/python-worker';
|
21
|
+
import { setupPluginRemote } from './plugin-remote';
|
22
|
+
import { ensurePluginVolume, getScryptedVolume } from './plugin-volume';
|
25
23
|
import { NodeForkWorker } from './runtime/node-fork-worker';
|
26
24
|
import { NodeThreadWorker } from './runtime/node-thread-worker';
|
25
|
+
import { PythonRuntimeWorker } from './runtime/python-worker';
|
26
|
+
import { RuntimeWorker } from './runtime/runtime-worker';
|
27
27
|
|
28
28
|
const serverVersion = require('../../package.json').version;
|
29
29
|
|
@@ -109,6 +109,7 @@ export class PluginHost {
|
|
109
109
|
// allow garbage collection of the base 64 contents
|
110
110
|
plugin = undefined;
|
111
111
|
|
112
|
+
const pluginDeviceId = scrypted.findPluginDevice(this.pluginId)._id;
|
112
113
|
const logger = scrypted.getDeviceLogger(scrypted.findPluginDevice(this.pluginId));
|
113
114
|
|
114
115
|
const volume = getScryptedVolume();
|
@@ -157,7 +158,7 @@ export class PluginHost {
|
|
157
158
|
|
158
159
|
const { runtime } = this.packageJson.scrypted;
|
159
160
|
const mediaManager = runtime === 'python'
|
160
|
-
? new MediaManagerHostImpl(scrypted.stateManager.getSystemState(), id => scrypted.getDevice(id)
|
161
|
+
? new MediaManagerHostImpl(pluginDeviceId, scrypted.stateManager.getSystemState(), console, id => scrypted.getDevice(id))
|
161
162
|
: undefined;
|
162
163
|
|
163
164
|
this.api = new PluginHostAPI(scrypted, this.pluginId, this, mediaManager);
|
@@ -94,15 +94,22 @@ export abstract class PluginHttp<T> {
|
|
94
94
|
}
|
95
95
|
|
96
96
|
if (!isEngineIOEndpoint && isUpgrade) {
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
97
|
+
try {
|
98
|
+
this.wss.handleUpgrade(req, req.socket, (req as any).upgradeHead, async (ws) => {
|
99
|
+
try {
|
100
|
+
await this.handleWebSocket(endpoint, httpRequest, ws, pluginData);
|
101
|
+
}
|
102
|
+
catch (e) {
|
103
|
+
console.error('websocket plugin error', e);
|
104
|
+
ws.close();
|
105
|
+
}
|
106
|
+
});
|
107
|
+
}
|
108
|
+
catch (e) {
|
109
|
+
res.status(500);
|
110
|
+
res.send(e.toString());
|
111
|
+
console.error(e);
|
112
|
+
}
|
106
113
|
}
|
107
114
|
else {
|
108
115
|
try {
|
@@ -1,13 +1,13 @@
|
|
1
|
+
import { DeviceManager, ScryptedNativeId, ScryptedStatic, SystemManager } from '@scrypted/types';
|
2
|
+
import { Console } from 'console';
|
3
|
+
import net from 'net';
|
4
|
+
import { install as installSourceMapSupport } from 'source-map-support';
|
5
|
+
import { PassThrough } from 'stream';
|
1
6
|
import { RpcMessage, RpcPeer } from '../rpc';
|
2
|
-
import { SystemManager, DeviceManager, ScryptedNativeId, ScryptedStatic } from '@scrypted/types'
|
3
|
-
import { attachPluginRemote, PluginReader } from './plugin-remote';
|
4
|
-
import { PluginAPI } from './plugin-api';
|
5
7
|
import { MediaManagerImpl } from './media';
|
6
|
-
import {
|
7
|
-
import { Console } from 'console'
|
8
|
-
import { install as installSourceMapSupport } from 'source-map-support';
|
9
|
-
import net from 'net'
|
8
|
+
import { PluginAPI } from './plugin-api';
|
10
9
|
import { installOptionalDependencies } from './plugin-npm-dependencies';
|
10
|
+
import { attachPluginRemote, PluginReader } from './plugin-remote';
|
11
11
|
import { createREPLServer } from './plugin-repl';
|
12
12
|
|
13
13
|
export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessage, reject?: (e: Error) => void) => void) {
|
@@ -172,9 +172,10 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
|
|
172
172
|
let postInstallSourceMapSupport: (scrypted: ScryptedStatic) => void;
|
173
173
|
|
174
174
|
attachPluginRemote(peer, {
|
175
|
-
createMediaManager: async (sm) => {
|
175
|
+
createMediaManager: async (sm, dm) => {
|
176
176
|
systemManager = sm;
|
177
|
-
|
177
|
+
deviceManager = dm
|
178
|
+
return new MediaManagerImpl(systemManager, dm);
|
178
179
|
},
|
179
180
|
onGetRemote: async (_api, _pluginId) => {
|
180
181
|
api = _api;
|
@@ -8,6 +8,8 @@ import { RpcPeer, RPCResultError } from '../rpc';
|
|
8
8
|
import { BufferSerializer } from './buffer-serializer';
|
9
9
|
import { createWebSocketClass, WebSocketConnectCallbacks, WebSocketMethods } from './plugin-remote-websocket';
|
10
10
|
import fs from 'fs';
|
11
|
+
import { checkProperty } from './plugin-state-check';
|
12
|
+
import _ from 'lodash';
|
11
13
|
const { link } = require('linkfs');
|
12
14
|
|
13
15
|
class DeviceLogger implements Logger {
|
@@ -115,12 +117,7 @@ class DeviceStateProxyHandler implements ProxyHandler<any> {
|
|
115
117
|
}
|
116
118
|
|
117
119
|
set?(target: any, p: PropertyKey, value: any, receiver: any) {
|
118
|
-
|
119
|
-
throw new Error("id is read only");
|
120
|
-
if (p === ScryptedInterfaceProperty.mixins)
|
121
|
-
throw new Error("mixins is read only");
|
122
|
-
if (p === ScryptedInterfaceProperty.interfaces)
|
123
|
-
throw new Error("interfaces is a read only post-mixin computed property, use providedInterfaces");
|
120
|
+
checkProperty(p.toString(), value);
|
124
121
|
const now = Date.now();
|
125
122
|
this.deviceManager.systemManager.state[this.id][p as string] = {
|
126
123
|
lastEventTime: now,
|
@@ -183,6 +180,21 @@ class DeviceManagerImpl implements DeviceManager {
|
|
183
180
|
}
|
184
181
|
return ret;
|
185
182
|
}
|
183
|
+
pruneMixinStorage() {
|
184
|
+
for (const nativeId of this.nativeIds.keys()) {
|
185
|
+
const storage = this.nativeIds.get(nativeId).storage;
|
186
|
+
for (const key of Object.keys(storage)) {
|
187
|
+
if (!key.startsWith('mixin:'))
|
188
|
+
continue;
|
189
|
+
const [, id,] = key.split(':');
|
190
|
+
// there's no rush to persist this, it will happen automatically on the plugin
|
191
|
+
// persisting something at some point.
|
192
|
+
// the key itself is unreachable due to the device no longer existing.
|
193
|
+
if (id && !this.systemManager.state[id])
|
194
|
+
delete storage[key];
|
195
|
+
}
|
196
|
+
}
|
197
|
+
}
|
186
198
|
async onMixinEvent(id: string, nativeId: ScryptedNativeId, eventInterface: string, eventData: any) {
|
187
199
|
return this.api.onMixinEvent(id, nativeId, eventInterface, eventData);
|
188
200
|
}
|
@@ -330,7 +342,7 @@ export interface WebSocketCustomHandler {
|
|
330
342
|
export type PluginReader = (name: string) => Buffer;
|
331
343
|
|
332
344
|
export interface PluginRemoteAttachOptions {
|
333
|
-
createMediaManager?: (systemManager: SystemManager) => Promise<MediaManager>;
|
345
|
+
createMediaManager?: (systemManager: SystemManager, deviceManager: DeviceManager) => Promise<MediaManager>;
|
334
346
|
getServicePort?: (name: string, ...args: any[]) => Promise<number>;
|
335
347
|
getDeviceConsole?: (nativeId?: ScryptedNativeId) => Console;
|
336
348
|
getPluginConsole?: () => Console;
|
@@ -354,7 +366,7 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
|
|
354
366
|
const systemManager = new SystemManagerImpl();
|
355
367
|
const deviceManager = new DeviceManagerImpl(systemManager, getDeviceConsole, getMixinConsole);
|
356
368
|
const endpointManager = new EndpointManagerImpl();
|
357
|
-
const mediaManager = await api.getMediaManager() || await createMediaManager(systemManager);
|
369
|
+
const mediaManager = await api.getMediaManager() || await createMediaManager(systemManager, deviceManager);
|
358
370
|
peer.params['mediaManager'] = mediaManager;
|
359
371
|
const ioSockets: { [id: string]: WebSocketConnectCallbacks } = {};
|
360
372
|
|
@@ -454,6 +466,7 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
|
|
454
466
|
|
455
467
|
async setSystemState(state: { [id: string]: { [property: string]: SystemDeviceState } }) {
|
456
468
|
systemManager.state = state;
|
469
|
+
deviceManager.pruneMixinStorage();
|
457
470
|
done(ret);
|
458
471
|
},
|
459
472
|
|
@@ -0,0 +1,14 @@
|
|
1
|
+
import { ScryptedInterface, ScryptedInterfaceProperty } from "@scrypted/types";
|
2
|
+
import { propertyInterfaces } from "./descriptor";
|
3
|
+
|
4
|
+
export function checkProperty(key: string, value: any) {
|
5
|
+
if (key === ScryptedInterfaceProperty.id)
|
6
|
+
throw new Error("id is read only");
|
7
|
+
if (key === ScryptedInterfaceProperty.mixins)
|
8
|
+
throw new Error("mixins is read only");
|
9
|
+
if (key === ScryptedInterfaceProperty.interfaces)
|
10
|
+
throw new Error("interfaces is a read only post-mixin computed property, use providedInterfaces");
|
11
|
+
const iface = propertyInterfaces[key.toString()];
|
12
|
+
if (iface === ScryptedInterface.ScryptedDevice)
|
13
|
+
throw new Error(`${key.toString()} can not be set. Use DeviceManager.onDevicesChanges or DeviceManager.onDeviceDiscovered to update the device description.`);
|
14
|
+
}
|
package/src/plugin/system.ts
CHANGED
@@ -50,10 +50,6 @@ class DeviceProxyHandler implements PrimitiveProxyHandler<any>, ScryptedDevice {
|
|
50
50
|
async apply(target: any, thisArg: any, argArray?: any) {
|
51
51
|
const method = target();
|
52
52
|
const device = await this.ensureDevice();
|
53
|
-
if (false && method === 'refresh') {
|
54
|
-
const name = this.systemManager.state[this.id]?.[ScryptedInterfaceProperty.name].value;
|
55
|
-
this.systemManager.log.i(`requested refresh ${name}`);
|
56
|
-
}
|
57
53
|
return (device as any)[method](...argArray);
|
58
54
|
}
|
59
55
|
|
package/src/runtime.ts
CHANGED
@@ -38,7 +38,7 @@ interface DeviceProxyPair {
|
|
38
38
|
proxy: ScryptedDevice;
|
39
39
|
}
|
40
40
|
|
41
|
-
const MIN_SCRYPTED_CORE_VERSION = 'v0.0.
|
41
|
+
const MIN_SCRYPTED_CORE_VERSION = 'v0.0.238';
|
42
42
|
const PLUGIN_DEVICE_STATE_VERSION = 2;
|
43
43
|
|
44
44
|
interface HttpPluginData {
|
@@ -431,7 +431,13 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
|
|
431
431
|
return ret;
|
432
432
|
}
|
433
433
|
|
434
|
-
async installNpm(pkg: string, version?: string): Promise<PluginHost> {
|
434
|
+
async installNpm(pkg: string, version?: string, installedSet?: Set<string>): Promise<PluginHost> {
|
435
|
+
if (!installedSet)
|
436
|
+
installedSet = new Set();
|
437
|
+
if (installedSet.has(pkg))
|
438
|
+
return;
|
439
|
+
installedSet.add(pkg);
|
440
|
+
|
435
441
|
const registry = (await axios(`https://registry.npmjs.org/${pkg}`)).data;
|
436
442
|
if (!version) {
|
437
443
|
version = registry['dist-tags'].latest;
|
@@ -463,6 +469,20 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
|
|
463
469
|
if (!packageJsonEntry)
|
464
470
|
throw new Error('package.json not found. are you behind a firewall?');
|
465
471
|
const packageJson = JSON.parse(packageJsonEntry.toString());
|
472
|
+
|
473
|
+
const pluginDependencies: string[] = packageJson.scrypted.pluginDependencies || [];
|
474
|
+
pluginDependencies.forEach(async (dep) => {
|
475
|
+
try {
|
476
|
+
const depId = this.findPluginDevice(dep);
|
477
|
+
if (depId)
|
478
|
+
throw new Error('Plugin already installed.');
|
479
|
+
await this.installNpm(dep);
|
480
|
+
}
|
481
|
+
catch (e) {
|
482
|
+
console.log('Skipping', dep, ':', e.message);
|
483
|
+
}
|
484
|
+
});
|
485
|
+
|
466
486
|
const npmPackage = packageJson.name;
|
467
487
|
const plugin = await this.datastore.tryGet(Plugin, npmPackage) || new Plugin();
|
468
488
|
|
@@ -745,6 +765,11 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
|
|
745
765
|
setState(pluginDevice, ScryptedInterfaceProperty.providedInterfaces, PluginDeviceProxyHandler.sortInterfaces(interfaces));
|
746
766
|
}
|
747
767
|
|
768
|
+
if (!pluginDevice.pluginId) {
|
769
|
+
dirty = true;
|
770
|
+
setState(pluginDevice, ScryptedInterfaceProperty.pluginId, pluginDevice.pluginId);
|
771
|
+
}
|
772
|
+
|
748
773
|
if (dirty) {
|
749
774
|
this.datastore.upsert(pluginDevice);
|
750
775
|
}
|
@@ -231,11 +231,6 @@ async function start() {
|
|
231
231
|
}
|
232
232
|
console.log(`Version: : ${await new Info().getVersion()}`);
|
233
233
|
console.log('#######################################################');
|
234
|
-
console.log('Chrome Users: You may need to type "thisisunsafe" into')
|
235
|
-
console.log(' the window to bypass the warning. There')
|
236
|
-
console.log(' may be no button to continue, type the')
|
237
|
-
console.log(' letters "thisisunsafe" and it will proceed.')
|
238
|
-
console.log('#######################################################');
|
239
234
|
console.log('Scrypted insecure http service port:', SCRYPTED_INSECURE_PORT);
|
240
235
|
console.log('Ports can be changed with environment variables.')
|
241
236
|
console.log('https: $SCRYPTED_SECURE_PORT')
|
package/src/services/plugin.ts
CHANGED
@@ -121,6 +121,11 @@ export class PluginComponent {
|
|
121
121
|
try {
|
122
122
|
const registry = await this.npmInfo(plugin);
|
123
123
|
const version = registry['dist-tags'].latest;
|
124
|
+
if (registry?.versions?.[version]?.deprecated) {
|
125
|
+
console.log('plugin deprecated, uninstalling:', plugin);
|
126
|
+
await this.scrypted.removeDevice(this.scrypted.findPluginDevice(plugin));
|
127
|
+
continue;
|
128
|
+
}
|
124
129
|
if (!semver.gt(version, host.packageJson.version)) {
|
125
130
|
console.log('plugin up to date:', plugin);
|
126
131
|
continue;
|
package/src/state.ts
CHANGED
@@ -169,9 +169,10 @@ export class ScryptedStateManager extends EventRegistry {
|
|
169
169
|
|
170
170
|
await sleep(timeout);
|
171
171
|
try {
|
172
|
-
|
172
|
+
const rt = this.refreshThrottles[id];
|
173
|
+
if (!rt.tailRefresh)
|
173
174
|
return;
|
174
|
-
await device[RefreshSymbol](
|
175
|
+
await device[RefreshSymbol](rt.refreshInterface, rt.userInitiated);
|
175
176
|
}
|
176
177
|
catch (e) {
|
177
178
|
logger.log('e', 'Refresh failed');
|