@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.

Files changed (43) hide show
  1. package/bin/scrypted-serve +0 -0
  2. package/dist/media-helpers.js +5 -1
  3. package/dist/media-helpers.js.map +1 -1
  4. package/dist/plugin/media.js +55 -26
  5. package/dist/plugin/media.js.map +1 -1
  6. package/dist/plugin/plugin-host-api.js +2 -0
  7. package/dist/plugin/plugin-host-api.js.map +1 -1
  8. package/dist/plugin/plugin-host.js +14 -13
  9. package/dist/plugin/plugin-host.js.map +1 -1
  10. package/dist/plugin/plugin-http.js +16 -9
  11. package/dist/plugin/plugin-http.js.map +1 -1
  12. package/dist/plugin/plugin-remote-worker.js +8 -7
  13. package/dist/plugin/plugin-remote-worker.js.map +1 -1
  14. package/dist/plugin/plugin-remote.js +19 -7
  15. package/dist/plugin/plugin-remote.js.map +1 -1
  16. package/dist/plugin/plugin-state-check.js +18 -0
  17. package/dist/plugin/plugin-state-check.js.map +1 -0
  18. package/dist/plugin/system.js +0 -4
  19. package/dist/plugin/system.js.map +1 -1
  20. package/dist/runtime.js +23 -2
  21. package/dist/runtime.js.map +1 -1
  22. package/dist/scrypted-server-main.js +0 -5
  23. package/dist/scrypted-server-main.js.map +1 -1
  24. package/dist/services/plugin.js +5 -0
  25. package/dist/services/plugin.js.map +1 -1
  26. package/dist/state.js +3 -2
  27. package/dist/state.js.map +1 -1
  28. package/package.json +2 -2
  29. package/python/plugin-remote.py +6 -6
  30. package/python/plugin-repl.py +9 -0
  31. package/src/media-helpers.ts +5 -1
  32. package/src/plugin/media.ts +68 -33
  33. package/src/plugin/plugin-host-api.ts +5 -2
  34. package/src/plugin/plugin-host.ts +20 -19
  35. package/src/plugin/plugin-http.ts +16 -9
  36. package/src/plugin/plugin-remote-worker.ts +10 -9
  37. package/src/plugin/plugin-remote.ts +21 -8
  38. package/src/plugin/plugin-state-check.ts +14 -0
  39. package/src/plugin/system.ts +0 -4
  40. package/src/runtime.ts +27 -2
  41. package/src/scrypted-server-main.ts +0 -5
  42. package/src/services/plugin.ts +5 -0
  43. package/src/state.ts +3 -2
@@ -0,0 +1,9 @@
1
+ from asyncore import write
2
+ import code
3
+
4
+ class ScryptedConsole(code.InteractiveConsole):
5
+ def write(self, data: str) -> None:
6
+ return super().write(data)
7
+
8
+ def raw_input(self, prompt: str) -> str:
9
+ return super().raw_input(prompt=prompt)
@@ -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
- cp.stdin.write('q\n');
14
+ try {
15
+ cp.stdin.write('q\n');
16
+ }
17
+ catch (e) {
18
+ }
15
19
  setTimeout(() => {
16
20
  cp.kill();
17
21
  setTimeout(() => {
@@ -1,19 +1,19 @@
1
- import { ScryptedInterfaceProperty, SystemDeviceState, MediaStreamUrl, BufferConverter, FFMpegInput, MediaManager, MediaObject, ScryptedInterface, ScryptedMimeTypes, SystemManager } from "@scrypted/types";
2
- import { MediaObjectRemote } from "./plugin-api";
3
- import mimeType from 'mime'
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 rimraf from "rimraf";
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(public console: Console) {
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 = mimeType.lookup(data.toString());
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: FFMpegInput = {
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: FFMpegInput = Object.assign({
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 ffInput: FFMpegInput = JSON.parse(data.toString());
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
- async createFFmpegMediaObject(ffMpegInput: FFMpegInput): Promise<MediaObject> {
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 createMediaObject(data: any, mimeType: string): Promise<MediaObject> {
270
- return this.createMediaObjectRemote(data, mimeType);
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, console: Console) {
396
- super(console);
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 systemState: { [id: string]: { [property: string]: SystemDeviceState } },
410
- public getDeviceById: (id: string) => any,
411
- console: Console) {
412
- super(console);
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 { RpcPeer } from '../rpc';
1
+ import { Device, EngineIOHandler } from '@scrypted/types';
2
2
  import AdmZip from 'adm-zip';
3
- import { Device, EngineIOHandler } from '@scrypted/types'
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 { setupPluginRemote } from './plugin-remote';
8
- import { PluginAPIProxy, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
9
- import { Logger } from '../logger';
10
- import { MediaManagerHostImpl } from './media';
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 { PluginHostAPI } from './plugin-host-api';
14
- import path from 'path';
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 crypto from 'crypto';
20
- import fs from 'fs';
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), console)
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
- this.wss.handleUpgrade(req, req.socket, (req as any).upgradeHead, async (ws) => {
98
- try {
99
- await this.handleWebSocket(endpoint, httpRequest, ws, pluginData);
100
- }
101
- catch (e) {
102
- console.error('websocket plugin error', e);
103
- ws.close();
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 { PassThrough } from 'stream';
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
- return new MediaManagerImpl(systemManager, getPluginConsole());
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
- if (p === ScryptedInterfaceProperty.id)
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
+ }
@@ -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.217';
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')
@@ -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
- if (!ret.tailRefresh)
172
+ const rt = this.refreshThrottles[id];
173
+ if (!rt.tailRefresh)
173
174
  return;
174
- await device[RefreshSymbol](ret.refreshInterface, ret.userInitiated);
175
+ await device[RefreshSymbol](rt.refreshInterface, rt.userInitiated);
175
176
  }
176
177
  catch (e) {
177
178
  logger.log('e', 'Refresh failed');