@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,27 +1,25 @@
1
- import { DeviceManager, ScryptedNativeId, ScryptedStatic, SystemManager } from '@scrypted/types';
1
+ import { ScryptedStatic, SystemManager } from '@scrypted/types';
2
2
  import AdmZip from 'adm-zip';
3
- import { Console } from 'console';
3
+ import { once } from 'events';
4
4
  import fs from 'fs';
5
5
  import { Volume } from 'memfs';
6
6
  import net from 'net';
7
7
  import path from 'path';
8
8
  import { install as installSourceMapSupport } from 'source-map-support';
9
- import { PassThrough } from 'stream';
9
+ import { listenZero } from '../listen-zero';
10
10
  import { RpcMessage, RpcPeer } from '../rpc';
11
+ import { createDuplexRpcPeer } from '../rpc-serializer';
11
12
  import { MediaManagerImpl } from './media';
12
13
  import { PluginAPI, PluginAPIProxy, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
14
+ import { prepareConsoles } from './plugin-console';
13
15
  import { installOptionalDependencies } from './plugin-npm-dependencies';
14
16
  import { attachPluginRemote, DeviceManagerImpl, PluginReader, setupPluginRemote } from './plugin-remote';
17
+ import { PluginStats, startStatsUpdater } from './plugin-remote-stats';
15
18
  import { createREPLServer } from './plugin-repl';
16
19
  import { NodeThreadWorker } from './runtime/node-thread-worker';
20
+ import crypto from 'crypto';
17
21
  const { link } = require('linkfs');
18
22
 
19
- interface PluginStats {
20
- type: 'stats',
21
- cpu: NodeJS.CpuUsage;
22
- memoryUsage: NodeJS.MemoryUsage;
23
- }
24
-
25
23
  const serverVersion = require('../../package.json').version;
26
24
 
27
25
  export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessage, reject?: (e: Error) => void, serializationContext?: any) => void) {
@@ -31,47 +29,6 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
31
29
  let deviceManager: DeviceManagerImpl;
32
30
  let api: PluginAPI;
33
31
 
34
- const getConsole = (hook: (stdout: PassThrough, stderr: PassThrough) => Promise<void>,
35
- also?: Console, alsoPrefix?: string) => {
36
-
37
- const stdout = new PassThrough();
38
- const stderr = new PassThrough();
39
-
40
- hook(stdout, stderr);
41
-
42
- const ret = new Console(stdout, stderr);
43
-
44
- const methods = [
45
- 'log', 'warn',
46
- 'dir', 'timeLog',
47
- 'trace', 'assert',
48
- 'clear', 'count',
49
- 'countReset', 'group',
50
- 'groupEnd', 'table',
51
- 'debug', 'info',
52
- 'dirxml', 'error',
53
- 'groupCollapsed',
54
- ];
55
-
56
- const printers = ['log', 'info', 'debug', 'trace', 'warn', 'error'];
57
- for (const m of methods) {
58
- const old = (ret as any)[m].bind(ret);
59
- (ret as any)[m] = (...args: any[]) => {
60
- // prefer the mixin version for local/remote console dump.
61
- if (also && alsoPrefix && printers.includes(m)) {
62
- (also as any)[m](alsoPrefix, ...args);
63
- }
64
- else {
65
- (console as any)[m](...args);
66
- }
67
- // call through to old method to ensure it gets written
68
- // to log buffer.
69
- old(...args);
70
- }
71
- }
72
-
73
- return ret;
74
- }
75
32
 
76
33
  let pluginsPromise: Promise<any>;
77
34
  function getPlugins() {
@@ -80,138 +37,13 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
80
37
  return pluginsPromise;
81
38
  }
82
39
 
83
- const deviceConsoles = new Map<string, Console>();
84
- const getDeviceConsole = (nativeId?: ScryptedNativeId) => {
85
- // the the plugin console is simply the default console
86
- // and gets read from stderr/stdout.
87
- if (!nativeId)
88
- return console;
89
-
90
- let ret = deviceConsoles.get(nativeId);
91
- if (ret)
92
- return ret;
93
-
94
- ret = getConsole(async (stdout, stderr) => {
95
- const connect = async () => {
96
- const plugins = await getPlugins();
97
- const port = await plugins.getRemoteServicePort(peer.selfName, 'console-writer');
98
- const socket = net.connect(port);
99
- socket.write(nativeId + '\n');
100
- const writer = (data: Buffer) => {
101
- socket.write(data);
102
- };
103
- stdout.on('data', writer);
104
- stderr.on('data', writer);
105
- socket.on('error', () => {
106
- stdout.removeAllListeners();
107
- stderr.removeAllListeners();
108
- stdout.pause();
109
- stderr.pause();
110
- setTimeout(connect, 10000);
111
- });
112
- };
113
- connect();
114
- }, undefined, undefined);
115
-
116
- deviceConsoles.set(nativeId, ret);
117
- return ret;
118
- }
119
-
120
- const mixinConsoles = new Map<string, Map<string, Console>>();
121
-
122
- const getMixinConsole = (mixinId: string, nativeId: ScryptedNativeId) => {
123
- let nativeIdConsoles = mixinConsoles.get(nativeId);
124
- if (!nativeIdConsoles) {
125
- nativeIdConsoles = new Map();
126
- mixinConsoles.set(nativeId, nativeIdConsoles);
127
- }
128
-
129
- let ret = nativeIdConsoles.get(mixinId);
130
- if (ret)
131
- return ret;
132
-
133
- ret = getConsole(async (stdout, stderr) => {
134
- if (!mixinId) {
135
- return;
136
- }
137
- const reconnect = () => {
138
- stdout.removeAllListeners();
139
- stderr.removeAllListeners();
140
- stdout.pause();
141
- stderr.pause();
142
- setTimeout(tryConnect, 10000);
143
- };
144
-
145
- const connect = async () => {
146
- const ds = deviceManager.getDeviceState(nativeId);
147
- if (!ds) {
148
- // deleted?
149
- return;
150
- }
151
-
152
- const plugins = await getPlugins();
153
- const { pluginId, nativeId: mixinNativeId } = await plugins.getDeviceInfo(mixinId);
154
- const port = await plugins.getRemoteServicePort(pluginId, 'console-writer');
155
- const socket = net.connect(port);
156
- socket.write(mixinNativeId + '\n');
157
- const writer = (data: Buffer) => {
158
- let str = data.toString().trim();
159
- str = str.replaceAll('\n', `\n[${ds.name}]: `);
160
- str = `[${ds.name}]: ` + str + '\n';
161
- socket.write(str);
162
- };
163
- stdout.on('data', writer);
164
- stderr.on('data', writer);
165
- socket.on('close', reconnect);
166
- };
167
-
168
- const tryConnect = async () => {
169
- try {
170
- await connect();
171
- }
172
- catch (e) {
173
- reconnect();
174
- }
175
- }
176
- tryConnect();
177
- }, getDeviceConsole(nativeId), `[${systemManager.getDeviceById(mixinId)?.name}]`);
178
-
179
- nativeIdConsoles.set(mixinId, ret);
180
- return ret;
181
- }
40
+ const { getDeviceConsole, getMixinConsole } = prepareConsoles(() => peer.selfName, () => systemManager, () => deviceManager, getPlugins);
182
41
 
183
42
  // process.cpuUsage is for the entire process.
184
43
  // process.memoryUsage is per thread.
185
44
  const allMemoryStats = new Map<NodeThreadWorker, NodeJS.MemoryUsage>();
186
45
 
187
- peer.getParam('updateStats').then((updateStats: (stats: PluginStats) => void) => {
188
- setInterval(() => {
189
- const cpuUsage = process.cpuUsage();
190
- allMemoryStats.set(undefined, process.memoryUsage());
191
-
192
- const memoryUsage: NodeJS.MemoryUsage = {
193
- rss: 0,
194
- heapTotal: 0,
195
- heapUsed: 0,
196
- external: 0,
197
- arrayBuffers: 0,
198
- }
199
-
200
- for (const mu of allMemoryStats.values()) {
201
- memoryUsage.rss += mu.rss;
202
- memoryUsage.heapTotal += mu.heapTotal;
203
- memoryUsage.heapUsed += mu.heapUsed;
204
- memoryUsage.external += mu.external;
205
- memoryUsage.arrayBuffers += mu.arrayBuffers;
206
- }
207
-
208
- updateStats({
209
- type: 'stats',
210
- cpu: cpuUsage,
211
- memoryUsage,
212
- });
213
- }, 10000);
214
- });
46
+ peer.getParam('updateStats').then(updateStats => startStatsUpdater(allMemoryStats, updateStats));
215
47
 
216
48
  let replPort: Promise<number>;
217
49
 
@@ -245,7 +77,74 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
245
77
  }
246
78
  throw new Error(`unknown service ${name}`);
247
79
  },
248
- async onLoadZip(scrypted: ScryptedStatic, params: any, packageJson: any, zipData: Buffer | string, zipOptions?: PluginRemoteLoadZipOptions) {
80
+ async onLoadZip(scrypted: ScryptedStatic, params: any, packageJson: any, zipData: Buffer | string, zipOptions: PluginRemoteLoadZipOptions) {
81
+ const { clusterId, clusterSecret } = zipOptions;
82
+ const clusterRpcServer = net.createServer(client => {
83
+ const clusterPeer = createDuplexRpcPeer(peer.selfName, 'cluster-client', client, client);
84
+ const portSecret = crypto.createHash('sha256').update(`${clusterPort}${clusterSecret}`).digest().toString('hex');
85
+ clusterPeer.params['connectRPCObject'] = async (id: string, secret: string) => {
86
+ if (secret !== portSecret)
87
+ throw new Error('secret incorrect');
88
+ return peer.localProxyMap.get(id);
89
+ }
90
+ client.on('close', () => clusterPeer.kill('cluster socket closed'));
91
+ })
92
+ const clusterPort = await listenZero(clusterRpcServer);
93
+ const clusterEntry = {
94
+ id: clusterId,
95
+ port: clusterPort,
96
+ };
97
+
98
+ peer.onProxySerialization = (value, proxyId) => {
99
+ const properties = RpcPeer.prepareProxyProperties(value) || {};
100
+ properties.__cluster = {
101
+ ...clusterEntry,
102
+ proxyId,
103
+ }
104
+ return properties;
105
+ }
106
+
107
+ const clusterPeers = new Map<number, Promise<RpcPeer>>();
108
+ scrypted.connectRPCObject = async (value: any) => {
109
+ const clusterObject = value?.__cluster;
110
+ if (clusterObject?.id !== clusterId)
111
+ return value;
112
+ const { port, proxyId } = clusterObject;
113
+
114
+ let clusterPeerPromise = clusterPeers.get(port);
115
+ if (!clusterPeerPromise) {
116
+ clusterPeerPromise = (async () => {
117
+ const socket = net.connect(port);
118
+ socket.on('close', () => clusterPeers.delete(port));
119
+
120
+ try {
121
+ await once(socket, 'connect');
122
+ const ret = createDuplexRpcPeer(peer.selfName, 'cluster-server', socket, socket);
123
+ return ret;
124
+ }
125
+ catch (e) {
126
+ console.error('failure ipc connect', e);
127
+ socket.destroy();
128
+ throw e;
129
+ }
130
+ })();
131
+ }
132
+
133
+ try {
134
+ const clusterPeer = await clusterPeerPromise;
135
+ const connectRPCObject = await clusterPeer.getParam('connectRPCObject');
136
+ const portSecret = crypto.createHash('sha256').update(`${port}${clusterSecret}`).digest().toString('hex');
137
+ const newValue = await connectRPCObject(proxyId, portSecret);
138
+ if (!newValue)
139
+ throw new Error('ipc object not found?');
140
+ return newValue;
141
+ }
142
+ catch (e) {
143
+ console.error('failure ipc', e);
144
+ return value;
145
+ }
146
+ }
147
+
249
148
  let volume: any;
250
149
  let pluginReader: PluginReader;
251
150
  if (zipOptions?.unzippedPath && fs.existsSync(zipOptions?.unzippedPath)) {
@@ -452,7 +452,7 @@ export interface PluginRemoteAttachOptions {
452
452
  getDeviceConsole?: (nativeId?: ScryptedNativeId) => Console;
453
453
  getPluginConsole?: () => Console;
454
454
  getMixinConsole?: (id: string, nativeId?: ScryptedNativeId) => Console;
455
- onLoadZip?: (scrypted: ScryptedStatic, params: any, packageJson: any, zipData: Buffer | string, zipOptions?: PluginRemoteLoadZipOptions) => Promise<any>;
455
+ onLoadZip?: (scrypted: ScryptedStatic, params: any, packageJson: any, zipData: Buffer | string, zipOptions: PluginRemoteLoadZipOptions) => Promise<any>;
456
456
  onGetRemote?: (api: PluginAPI, pluginId: string) => Promise<void>;
457
457
  }
458
458
 
@@ -37,13 +37,16 @@ export async function createREPLServer(scrypted: ScryptedStatic, params: any, pl
37
37
  const ctx = Object.assign(params, {
38
38
  device,
39
39
  realDevice,
40
+ sdk: scrypted,
40
41
  });
41
42
  delete ctx.console;
42
43
  delete ctx.window;
43
44
  delete ctx.WebSocket;
44
45
  delete ctx.pluginHostAPI;
46
+ delete ctx.log;
47
+ delete ctx.pluginRuntimeAPI;
45
48
 
46
- const replFilter = new Set<string>(['require', 'localStorage', 'exports', '__filename'])
49
+ const replFilter = new Set<string>(['require', 'localStorage', 'exports', '__filename', 'log'])
47
50
  const replVariables = Object.keys(ctx).filter(key => !replFilter.has(key));
48
51
 
49
52
  const welcome = `JavaScript REPL variables:\n${replVariables.map(key => ' ' + key).join('\n')}\n\n`;
@@ -56,6 +56,8 @@ export class PythonRuntimeWorker extends ChildProcessWorker {
56
56
  pythonPath ||= 'python3';
57
57
  }
58
58
 
59
+ args.push(this.pluginId);
60
+
59
61
  this.worker = child_process.spawn(pythonPath, args, {
60
62
  // stdin, stdout, stderr, peer in, peer out
61
63
  stdio: ['pipe', 'pipe', 'pipe', 'pipe', 'pipe'],
@@ -1,6 +1,7 @@
1
1
  import { EventListener, EventListenerOptions, EventListenerRegister, Logger, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceDescriptor, ScryptedInterfaceDescriptors, ScryptedInterfaceProperty, SystemDeviceState, SystemManager } from "@scrypted/types";
2
2
  import { EventRegistry } from "../event-registry";
3
3
  import { PrimitiveProxyHandler, RpcPeer } from '../rpc';
4
+ import type { PluginComponent } from "../services/plugin";
4
5
  import { getInterfaceMethods, getInterfaceProperties, getPropertyInterfaces, isValidInterfaceMethod, propertyInterfaces } from "./descriptor";
5
6
  import { PluginAPI } from "./plugin-api";
6
7
 
@@ -120,6 +121,11 @@ class DeviceProxyHandler implements PrimitiveProxyHandler<any>, ScryptedDevice {
120
121
  return this.systemManager.api.setDeviceProperty(this.id, ScryptedInterfaceProperty.type, type);
121
122
  }
122
123
 
124
+ async setMixins(mixins: string[]) {
125
+ const plugins = await this.systemManager.getComponent('plugins') as PluginComponent;
126
+ await plugins.setMixins(this.id, mixins);
127
+ }
128
+
123
129
  async probe(): Promise<boolean> {
124
130
  return this.apply(() => 'probe', undefined, []);
125
131
  }