@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.
- package/dist/http-interfaces.js +4 -1
- package/dist/http-interfaces.js.map +1 -1
- package/dist/listen-zero.js +5 -2
- package/dist/listen-zero.js.map +1 -1
- package/dist/plugin/media.js +25 -20
- package/dist/plugin/media.js.map +1 -1
- package/dist/plugin/plugin-console.js +157 -4
- package/dist/plugin/plugin-console.js.map +1 -1
- package/dist/plugin/plugin-device.js +2 -0
- package/dist/plugin/plugin-device.js.map +1 -1
- package/dist/plugin/plugin-host.js +5 -0
- package/dist/plugin/plugin-host.js.map +1 -1
- package/dist/plugin/plugin-remote-stats.js +30 -0
- package/dist/plugin/plugin-remote-stats.js.map +1 -0
- package/dist/plugin/plugin-remote-worker.js +69 -149
- package/dist/plugin/plugin-remote-worker.js.map +1 -1
- package/dist/plugin/plugin-repl.js +4 -1
- package/dist/plugin/plugin-repl.js.map +1 -1
- package/dist/plugin/runtime/python-worker.js +1 -0
- package/dist/plugin/runtime/python-worker.js.map +1 -1
- package/dist/plugin/system.js +4 -0
- package/dist/plugin/system.js.map +1 -1
- package/dist/rpc.js +183 -45
- package/dist/rpc.js.map +1 -1
- package/dist/runtime.js +3 -0
- package/dist/runtime.js.map +1 -1
- package/dist/threading.js +1 -0
- package/dist/threading.js.map +1 -1
- package/package.json +3 -4
- package/python/plugin_remote.py +134 -51
- package/python/rpc-iterator-test.py +45 -0
- package/python/rpc.py +168 -50
- package/python/rpc_reader.py +57 -60
- package/src/http-interfaces.ts +5 -1
- package/src/listen-zero.ts +6 -2
- package/src/plugin/media.ts +38 -35
- package/src/plugin/plugin-api.ts +4 -1
- package/src/plugin/plugin-console.ts +154 -6
- package/src/plugin/plugin-device.ts +3 -0
- package/src/plugin/plugin-host.ts +5 -0
- package/src/plugin/plugin-remote-stats.ts +36 -0
- package/src/plugin/plugin-remote-worker.ts +77 -178
- package/src/plugin/plugin-remote.ts +1 -1
- package/src/plugin/plugin-repl.ts +4 -1
- package/src/plugin/runtime/python-worker.ts +2 -0
- package/src/plugin/system.ts +6 -0
- package/src/rpc.ts +230 -52
- package/src/runtime.ts +3 -0
- package/src/threading.ts +2 -0
- package/test/rpc-iterator-test.ts +46 -0
- package/test/rpc-python-test.ts +44 -0
@@ -1,27 +1,25 @@
|
|
1
|
-
import {
|
1
|
+
import { ScryptedStatic, SystemManager } from '@scrypted/types';
|
2
2
|
import AdmZip from 'adm-zip';
|
3
|
-
import {
|
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 {
|
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
|
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(
|
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
|
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
|
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'],
|
package/src/plugin/system.ts
CHANGED
@@ -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
|
}
|