@scrypted/server 0.6.24 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @scrypted/server might be problematic. Click here for more details.

Files changed (51) hide show
  1. package/dist/http-interfaces.js +4 -1
  2. package/dist/http-interfaces.js.map +1 -1
  3. package/dist/listen-zero.js +5 -2
  4. package/dist/listen-zero.js.map +1 -1
  5. package/dist/plugin/media.js +25 -20
  6. package/dist/plugin/media.js.map +1 -1
  7. package/dist/plugin/plugin-console.js +157 -4
  8. package/dist/plugin/plugin-console.js.map +1 -1
  9. package/dist/plugin/plugin-device.js +2 -0
  10. package/dist/plugin/plugin-device.js.map +1 -1
  11. package/dist/plugin/plugin-host.js +5 -0
  12. package/dist/plugin/plugin-host.js.map +1 -1
  13. package/dist/plugin/plugin-remote-stats.js +30 -0
  14. package/dist/plugin/plugin-remote-stats.js.map +1 -0
  15. package/dist/plugin/plugin-remote-worker.js +69 -149
  16. package/dist/plugin/plugin-remote-worker.js.map +1 -1
  17. package/dist/plugin/plugin-repl.js +4 -1
  18. package/dist/plugin/plugin-repl.js.map +1 -1
  19. package/dist/plugin/runtime/python-worker.js +1 -0
  20. package/dist/plugin/runtime/python-worker.js.map +1 -1
  21. package/dist/plugin/system.js +4 -0
  22. package/dist/plugin/system.js.map +1 -1
  23. package/dist/rpc.js +183 -45
  24. package/dist/rpc.js.map +1 -1
  25. package/dist/runtime.js +3 -0
  26. package/dist/runtime.js.map +1 -1
  27. package/dist/threading.js +1 -0
  28. package/dist/threading.js.map +1 -1
  29. package/package.json +3 -4
  30. package/python/plugin_remote.py +134 -51
  31. package/python/rpc-iterator-test.py +45 -0
  32. package/python/rpc.py +168 -50
  33. package/python/rpc_reader.py +57 -60
  34. package/src/http-interfaces.ts +5 -1
  35. package/src/listen-zero.ts +6 -2
  36. package/src/plugin/media.ts +38 -35
  37. package/src/plugin/plugin-api.ts +4 -1
  38. package/src/plugin/plugin-console.ts +154 -6
  39. package/src/plugin/plugin-device.ts +3 -0
  40. package/src/plugin/plugin-host.ts +5 -0
  41. package/src/plugin/plugin-remote-stats.ts +36 -0
  42. package/src/plugin/plugin-remote-worker.ts +77 -178
  43. package/src/plugin/plugin-remote.ts +1 -1
  44. package/src/plugin/plugin-repl.ts +4 -1
  45. package/src/plugin/runtime/python-worker.ts +2 -0
  46. package/src/plugin/system.ts +6 -0
  47. package/src/rpc.ts +230 -52
  48. package/src/runtime.ts +3 -0
  49. package/src/threading.ts +2 -0
  50. package/test/rpc-iterator-test.ts +46 -0
  51. package/test/rpc-python-test.ts +44 -0
@@ -1,4 +1,4 @@
1
- import { BufferConverter, DeviceManager, FFmpegInput, MediaManager, MediaObject, MediaObjectOptions, MediaStreamUrl, ScryptedDevice, ScryptedInterface, ScryptedInterfaceProperty, ScryptedMimeTypes, ScryptedNativeId, SystemDeviceState, SystemManager } from "@scrypted/types";
1
+ import { BufferConverter, DeviceManager, FFmpegInput, MediaManager, MediaObject as MediaObjectInterface, MediaObjectOptions, MediaStreamUrl, ScryptedDevice, ScryptedInterface, ScryptedInterfaceProperty, ScryptedMimeTypes, ScryptedNativeId, SystemDeviceState, SystemManager } from "@scrypted/types";
2
2
  import axios from 'axios';
3
3
  import pathToFfmpeg from 'ffmpeg-static';
4
4
  import fs from 'fs';
@@ -9,8 +9,28 @@ import Graph from 'node-dijkstra';
9
9
  import os from 'os';
10
10
  import path from 'path';
11
11
  import MimeType from 'whatwg-mimetype';
12
+ import { RpcPeer } from "../rpc";
12
13
  import { MediaObjectRemote } from "./plugin-api";
13
14
 
15
+ class MediaObject implements MediaObjectRemote {
16
+ __proxy_props: any;
17
+
18
+ constructor(public mimeType: string, public data: any, options: MediaObjectOptions) {
19
+ this.__proxy_props = {}
20
+ options ||= {};
21
+ options.mimeType = mimeType;
22
+ for (const [key, value] of Object.entries(options)) {
23
+ if (RpcPeer.isTransportSafe(value))
24
+ this.__proxy_props[key] = value;
25
+ (this as any)[key] = value;
26
+ }
27
+ }
28
+
29
+ async getData(): Promise<Buffer | string> {
30
+ return Promise.resolve(this.data);
31
+ }
32
+ }
33
+
14
34
  function typeMatches(target: string, candidate: string): boolean {
15
35
  // candidate will accept anything
16
36
  if (candidate === '*' || target === '*')
@@ -164,7 +184,7 @@ export abstract class MediaManagerBase implements MediaManager {
164
184
  this.extraConverters = [];
165
185
  }
166
186
 
167
- async convertMediaObjectToJSON<T>(mediaObject: MediaObject, toMimeType: string): Promise<T> {
187
+ async convertMediaObjectToJSON<T>(mediaObject: MediaObjectInterface, toMimeType: string): Promise<T> {
168
188
  const buffer = await this.convertMediaObjectToBuffer(mediaObject, toMimeType);
169
189
  return JSON.parse(buffer.toString());
170
190
  }
@@ -231,7 +251,7 @@ export abstract class MediaManagerBase implements MediaManager {
231
251
  return converters;
232
252
  }
233
253
 
234
- ensureMediaObjectRemote(mediaObject: string | MediaObject): MediaObjectRemote {
254
+ ensureMediaObjectRemote(mediaObject: string | MediaObjectInterface): MediaObjectRemote {
235
255
  if (typeof mediaObject === 'string') {
236
256
  const mime = mimeType.getType(mediaObject);
237
257
  return this.createMediaObjectRemote(mediaObject, mime);
@@ -239,36 +259,36 @@ export abstract class MediaManagerBase implements MediaManager {
239
259
  return mediaObject as MediaObjectRemote;
240
260
  }
241
261
 
242
- async convertMediaObject<T>(mediaObject: MediaObject, toMimeType: string): Promise<T> {
262
+ async convertMediaObject<T>(mediaObject: MediaObjectInterface, toMimeType: string): Promise<T> {
243
263
  const converted = await this.convert(this.getConverters(), this.ensureMediaObjectRemote(mediaObject), toMimeType);
244
264
  return converted.data;
245
265
  }
246
266
 
247
- async convertMediaObjectToInsecureLocalUrl(mediaObject: string | MediaObject, toMimeType: string): Promise<string> {
267
+ async convertMediaObjectToInsecureLocalUrl(mediaObject: string | MediaObjectInterface, toMimeType: string): Promise<string> {
248
268
  const intermediate = await this.convert(this.getConverters(), this.ensureMediaObjectRemote(mediaObject), toMimeType);
249
269
  const converted = this.createMediaObjectRemote(intermediate.data, intermediate.mimeType);
250
270
  const url = await this.convert(this.getConverters(), converted, ScryptedMimeTypes.InsecureLocalUrl);
251
271
  return url.data.toString();
252
272
  }
253
273
 
254
- async convertMediaObjectToBuffer(mediaObject: MediaObject, toMimeType: string): Promise<Buffer> {
274
+ async convertMediaObjectToBuffer(mediaObject: MediaObjectInterface, toMimeType: string): Promise<Buffer> {
255
275
  const intermediate = await this.convert(this.getConverters(), this.ensureMediaObjectRemote(mediaObject), toMimeType);
256
276
  return intermediate.data as Buffer;
257
277
  }
258
- async convertMediaObjectToLocalUrl(mediaObject: string | MediaObject, toMimeType: string): Promise<string> {
278
+ async convertMediaObjectToLocalUrl(mediaObject: string | MediaObjectInterface, toMimeType: string): Promise<string> {
259
279
  const intermediate = await this.convert(this.getConverters(), this.ensureMediaObjectRemote(mediaObject), toMimeType);
260
280
  const converted = this.createMediaObjectRemote(intermediate.data, intermediate.mimeType);
261
281
  const url = await this.convert(this.getConverters(), converted, ScryptedMimeTypes.LocalUrl);
262
282
  return url.data.toString();
263
283
  }
264
- async convertMediaObjectToUrl(mediaObject: string | MediaObject, toMimeType: string): Promise<string> {
284
+ async convertMediaObjectToUrl(mediaObject: string | MediaObjectInterface, toMimeType: string): Promise<string> {
265
285
  const intermediate = await this.convert(this.getConverters(), this.ensureMediaObjectRemote(mediaObject), toMimeType);
266
286
  const converted = this.createMediaObjectRemote(intermediate.data, intermediate.mimeType);
267
287
  const url = await this.convert(this.getConverters(), converted, ScryptedMimeTypes.Url);
268
288
  return url.data.toString();
269
289
  }
270
290
 
271
- createMediaObjectRemote(data: any | Buffer | Promise<string | Buffer>, mimeType: string, options?: MediaObjectOptions): MediaObjectRemote {
291
+ createMediaObjectRemote<T extends MediaObjectOptions>(data: any | Buffer | Promise<string | Buffer>, mimeType: string, options?: T): MediaObjectRemote & T {
272
292
  if (typeof data === 'string')
273
293
  throw new Error('string is not a valid type. if you intended to send a url, use createMediaObjectFromUrl.');
274
294
  if (!mimeType)
@@ -280,26 +300,19 @@ export abstract class MediaManagerBase implements MediaManager {
280
300
  data = Buffer.from(JSON.stringify(data));
281
301
 
282
302
  const sourceId = typeof options?.sourceId === 'string' ? options?.sourceId : this.getPluginDeviceId();
283
- class MediaObjectImpl implements MediaObjectRemote {
284
- __proxy_props = {
285
- mimeType,
286
- sourceId,
287
- }
288
-
289
- mimeType = mimeType;
290
- sourceId = sourceId;
291
- async getData(): Promise<Buffer | string> {
292
- return Promise.resolve(data);
293
- }
303
+ if (sourceId) {
304
+ options ||= {} as T;
305
+ options.sourceId = sourceId;
294
306
  }
295
- return new MediaObjectImpl();
307
+
308
+ return new MediaObject(mimeType, data, options) as MediaObject & T;
296
309
  }
297
310
 
298
- async createFFmpegMediaObject(ffMpegInput: FFmpegInput, options?: MediaObjectOptions): Promise<MediaObject> {
311
+ async createFFmpegMediaObject(ffMpegInput: FFmpegInput, options?: MediaObjectOptions): Promise<MediaObjectInterface> {
299
312
  return this.createMediaObjectRemote(Buffer.from(JSON.stringify(ffMpegInput)), ScryptedMimeTypes.FFmpegInput, options);
300
313
  }
301
314
 
302
- async createMediaObjectFromUrl(data: string, options?: MediaObjectOptions): Promise<MediaObject> {
315
+ async createMediaObjectFromUrl(data: string, options?: MediaObjectOptions): Promise<MediaObjectInterface> {
303
316
  const url = new URL(data);
304
317
  const scheme = url.protocol.slice(0, -1);
305
318
  const mimeType = ScryptedMimeTypes.SchemePrefix + scheme;
@@ -320,7 +333,7 @@ export abstract class MediaManagerBase implements MediaManager {
320
333
  return new MediaObjectImpl();
321
334
  }
322
335
 
323
- async createMediaObject(data: any, mimeType: string, options?: MediaObjectOptions): Promise<MediaObject> {
336
+ async createMediaObject<T extends MediaObjectOptions>(data: any, mimeType: string, options?: T): Promise<MediaObjectInterface & T> {
324
337
  return this.createMediaObjectRemote(data, mimeType, options);
325
338
  }
326
339
 
@@ -423,7 +436,7 @@ export abstract class MediaManagerBase implements MediaManager {
423
436
  }
424
437
 
425
438
  if (converter.toMimeType === ScryptedMimeTypes.MediaObject) {
426
- const mo = await converter.convert(value, valueMime.essence, toMimeType, { sourceId }) as MediaObject;
439
+ const mo = await converter.convert(value, valueMime.essence, toMimeType, { sourceId }) as MediaObjectInterface;
427
440
  const found = await this.convertMediaObject(mo, toMimeType);
428
441
  return {
429
442
  data: found,
@@ -445,16 +458,6 @@ export abstract class MediaManagerBase implements MediaManager {
445
458
  export class MediaManagerImpl extends MediaManagerBase {
446
459
  constructor(public systemManager: SystemManager, public deviceManager: DeviceManager) {
447
460
  super();
448
-
449
- this.builtinConverters.push({
450
- id: getBuiltinId(this.builtinConverters.length),
451
- name: 'ScryptedDeviceId to ScryptedDevice Converter',
452
- fromMimeType: ScryptedMimeTypes.ScryptedDeviceId,
453
- toMimeType: ScryptedMimeTypes.ScryptedDevice,
454
- convert: async (data, fromMimeType, toMimeType) => {
455
- return this.getDeviceById(data.toString());
456
- }
457
- });
458
461
  }
459
462
 
460
463
  getSystemState(): { [id: string]: { [property: string]: SystemDeviceState; }; } {
@@ -168,10 +168,13 @@ export interface PluginRemoteLoadZipOptions {
168
168
  */
169
169
  unzippedPath?: string;
170
170
  fork?: boolean;
171
+
172
+ clusterId: string;
173
+ clusterSecret: string;
171
174
  }
172
175
 
173
176
  export interface PluginRemote {
174
- loadZip(packageJson: any, zipData: Buffer | string, options?: PluginRemoteLoadZipOptions): Promise<any>;
177
+ loadZip(packageJson: any, zipData: Buffer | string, options: PluginRemoteLoadZipOptions): Promise<any>;
175
178
  setSystemState(state: { [id: string]: { [property: string]: SystemDeviceState } }): Promise<void>;
176
179
  setNativeId(nativeId: ScryptedNativeId, id: string, storage: { [key: string]: any }): Promise<void>;
177
180
  updateDeviceState(id: string, state: { [property: string]: SystemDeviceState }): Promise<void>;
@@ -1,10 +1,9 @@
1
- import { ScryptedNativeId } from '@scrypted/types'
2
- import { listenZero } from '../listen-zero';
3
- import { Server } from 'net';
4
- import { once } from 'events';
5
- import net from 'net'
6
- import { Readable, PassThrough } from 'stream';
1
+ import { DeviceManager, ScryptedNativeId, SystemManager } from '@scrypted/types';
7
2
  import { Console } from 'console';
3
+ import { once } from 'events';
4
+ import net, { Server } from 'net';
5
+ import { PassThrough, Readable } from 'stream';
6
+ import { listenZero } from '../listen-zero';
8
7
 
9
8
  export interface ConsoleServer {
10
9
  pluginConsole: Console;
@@ -20,6 +19,155 @@ export interface StdPassThroughs {
20
19
  buffers: Buffer[];
21
20
  }
22
21
 
22
+ export function getConsole(hook: (stdout: PassThrough, stderr: PassThrough) => Promise<void>,
23
+ also?: Console, alsoPrefix?: string) {
24
+
25
+ const stdout = new PassThrough();
26
+ const stderr = new PassThrough();
27
+
28
+ hook(stdout, stderr);
29
+
30
+ const ret = new Console(stdout, stderr);
31
+
32
+ const methods = [
33
+ 'log', 'warn',
34
+ 'dir', 'timeLog',
35
+ 'trace', 'assert',
36
+ 'clear', 'count',
37
+ 'countReset', 'group',
38
+ 'groupEnd', 'table',
39
+ 'debug', 'info',
40
+ 'dirxml', 'error',
41
+ 'groupCollapsed',
42
+ ];
43
+
44
+ const printers = ['log', 'info', 'debug', 'trace', 'warn', 'error'];
45
+ for (const m of methods) {
46
+ const old = (ret as any)[m].bind(ret);
47
+ (ret as any)[m] = (...args: any[]) => {
48
+ // prefer the mixin version for local/remote console dump.
49
+ if (also && alsoPrefix && printers.includes(m)) {
50
+ (also as any)[m](alsoPrefix, ...args);
51
+ }
52
+ else {
53
+ (console as any)[m](...args);
54
+ }
55
+ // call through to old method to ensure it gets written
56
+ // to log buffer.
57
+ old(...args);
58
+ }
59
+ }
60
+
61
+ return ret;
62
+ }
63
+
64
+ export function prepareConsoles(getConsoleName: () => string, systemManager: () => SystemManager, deviceManager: () => DeviceManager, getPlugins: () => Promise<any>) {
65
+ const deviceConsoles = new Map<string, Console>();
66
+ function getDeviceConsole (nativeId?: ScryptedNativeId) {
67
+ // the the plugin console is simply the default console
68
+ // and gets read from stderr/stdout.
69
+ if (!nativeId)
70
+ return console;
71
+
72
+ let ret = deviceConsoles.get(nativeId);
73
+ if (ret)
74
+ return ret;
75
+
76
+ ret = getConsole(async (stdout, stderr) => {
77
+ const connect = async () => {
78
+ const plugins = await getPlugins();
79
+ const port = await plugins.getRemoteServicePort(getConsoleName(), 'console-writer');
80
+ const socket = net.connect(port);
81
+ socket.write(nativeId + '\n');
82
+ const writer = (data: Buffer) => {
83
+ socket.write(data);
84
+ };
85
+ stdout.on('data', writer);
86
+ stderr.on('data', writer);
87
+ socket.on('error', () => {
88
+ stdout.removeAllListeners();
89
+ stderr.removeAllListeners();
90
+ stdout.pause();
91
+ stderr.pause();
92
+ setTimeout(connect, 10000);
93
+ });
94
+ };
95
+ connect();
96
+ }, undefined, undefined);
97
+
98
+ deviceConsoles.set(nativeId, ret);
99
+ return ret;
100
+ }
101
+
102
+ const mixinConsoles = new Map<string, Map<string, Console>>();
103
+
104
+ function getMixinConsole(mixinId: string, nativeId: ScryptedNativeId) {
105
+ let nativeIdConsoles = mixinConsoles.get(nativeId);
106
+ if (!nativeIdConsoles) {
107
+ nativeIdConsoles = new Map();
108
+ mixinConsoles.set(nativeId, nativeIdConsoles);
109
+ }
110
+
111
+ let ret = nativeIdConsoles.get(mixinId);
112
+ if (ret)
113
+ return ret;
114
+
115
+ ret = getConsole(async (stdout, stderr) => {
116
+ if (!mixinId) {
117
+ return;
118
+ }
119
+ const reconnect = () => {
120
+ stdout.removeAllListeners();
121
+ stderr.removeAllListeners();
122
+ stdout.pause();
123
+ stderr.pause();
124
+ setTimeout(tryConnect, 10000);
125
+ };
126
+
127
+ const connect = async () => {
128
+ const ds = deviceManager().getDeviceState(nativeId);
129
+ if (!ds) {
130
+ // deleted?
131
+ return;
132
+ }
133
+
134
+ const plugins = await getPlugins();
135
+ const { pluginId, nativeId: mixinNativeId } = await plugins.getDeviceInfo(mixinId);
136
+ const port = await plugins.getRemoteServicePort(pluginId, 'console-writer');
137
+ const socket = net.connect(port);
138
+ socket.write(mixinNativeId + '\n');
139
+ const writer = (data: Buffer) => {
140
+ let str = data.toString().trim();
141
+ str = str.replaceAll('\n', `\n[${ds.name}]: `);
142
+ str = `[${ds.name}]: ` + str + '\n';
143
+ socket.write(str);
144
+ };
145
+ stdout.on('data', writer);
146
+ stderr.on('data', writer);
147
+ socket.on('close', reconnect);
148
+ };
149
+
150
+ const tryConnect = async () => {
151
+ try {
152
+ await connect();
153
+ }
154
+ catch (e) {
155
+ reconnect();
156
+ }
157
+ }
158
+ tryConnect();
159
+ }, getDeviceConsole(nativeId), `[${systemManager().getDeviceById(mixinId)?.name}]`);
160
+
161
+ nativeIdConsoles.set(mixinId, ret);
162
+ return ret;
163
+ }
164
+
165
+ return {
166
+ getDeviceConsole,
167
+ getMixinConsole,
168
+ }
169
+ }
170
+
23
171
  export async function createConsoleServer(remoteStdout: Readable, remoteStderr: Readable, header: string) {
24
172
  const outputs = new Map<string, StdPassThroughs>();
25
173
 
@@ -352,6 +352,9 @@ export class PluginDeviceProxyHandler implements PrimitiveProxyHandler<any>, Scr
352
352
  this.scrypted.stateManager.setPluginDeviceState(device, ScryptedInterfaceProperty.type, type);
353
353
  this.scrypted.stateManager.updateDescriptor(device);
354
354
  }
355
+ async setMixins(mixins: string[]): Promise<void> {
356
+
357
+ }
355
358
 
356
359
  async probe(): Promise<boolean> {
357
360
  try {
@@ -41,6 +41,9 @@ export class PluginHost {
41
41
  scrypted: ScryptedRuntime;
42
42
  remote: PluginRemote;
43
43
  io: IOServer = new io.Server({
44
+ // object detection drag drop 4k can be massive.
45
+ // streaming support somehow?
46
+ maxHttpBufferSize: 20000000,
44
47
  pingTimeout: 120000,
45
48
  perMessageDeflate: true,
46
49
  cors: (req, callback) => {
@@ -242,6 +245,8 @@ export class PluginHost {
242
245
  try {
243
246
  const isPython = runtime === 'python';
244
247
  const loadZipOptions: PluginRemoteLoadZipOptions = {
248
+ clusterId: scrypted.clusterId,
249
+ clusterSecret: scrypted.clusterSecret,
245
250
  // if debugging, use a normalized path for sourcemap resolution, otherwise
246
251
  // prefix with module path.
247
252
  filename: isPython
@@ -0,0 +1,36 @@
1
+ import { NodeThreadWorker } from "./runtime/node-thread-worker";
2
+
3
+ export interface PluginStats {
4
+ type: 'stats',
5
+ cpu: NodeJS.CpuUsage;
6
+ memoryUsage: NodeJS.MemoryUsage;
7
+ }
8
+
9
+ export function startStatsUpdater(allMemoryStats: Map<NodeThreadWorker, NodeJS.MemoryUsage>, updateStats: (stats: PluginStats) => void) {
10
+ setInterval(() => {
11
+ const cpuUsage = process.cpuUsage();
12
+ allMemoryStats.set(undefined, process.memoryUsage());
13
+
14
+ const memoryUsage: NodeJS.MemoryUsage = {
15
+ rss: 0,
16
+ heapTotal: 0,
17
+ heapUsed: 0,
18
+ external: 0,
19
+ arrayBuffers: 0,
20
+ }
21
+
22
+ for (const mu of allMemoryStats.values()) {
23
+ memoryUsage.rss += mu.rss;
24
+ memoryUsage.heapTotal += mu.heapTotal;
25
+ memoryUsage.heapUsed += mu.heapUsed;
26
+ memoryUsage.external += mu.external;
27
+ memoryUsage.arrayBuffers += mu.arrayBuffers;
28
+ }
29
+
30
+ updateStats({
31
+ type: 'stats',
32
+ cpu: cpuUsage,
33
+ memoryUsage,
34
+ });
35
+ }, 10000);
36
+ }