@scrypted/server 0.0.113 → 0.0.118
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/.vscode/launch.json +1 -0
- package/dist/http-interfaces.js +11 -4
- package/dist/http-interfaces.js.map +1 -1
- package/dist/level.js.map +1 -1
- package/dist/plugin/media.js +3 -3
- package/dist/plugin/media.js.map +1 -1
- package/dist/plugin/plugin-console.js.map +1 -1
- package/dist/plugin/plugin-device.js +25 -0
- package/dist/plugin/plugin-device.js.map +1 -1
- package/dist/plugin/plugin-host-api.js +2 -2
- package/dist/plugin/plugin-host-api.js.map +1 -1
- package/dist/plugin/plugin-host.js +111 -227
- package/dist/plugin/plugin-host.js.map +1 -1
- package/dist/plugin/plugin-lazy-remote.js.map +1 -1
- package/dist/plugin/plugin-npm-dependencies.js +8 -3
- package/dist/plugin/plugin-npm-dependencies.js.map +1 -1
- package/dist/plugin/plugin-remote-worker.js +253 -0
- package/dist/plugin/plugin-remote-worker.js.map +1 -0
- package/dist/plugin/plugin-remote.js +39 -15
- package/dist/plugin/plugin-remote.js.map +1 -1
- package/dist/plugin/plugin-repl.js +3 -1
- package/dist/plugin/plugin-repl.js.map +1 -1
- package/dist/plugin/system.js +11 -6
- package/dist/plugin/system.js.map +1 -1
- package/dist/rpc.js +11 -1
- package/dist/rpc.js.map +1 -1
- package/dist/runtime.js +19 -4
- package/dist/runtime.js.map +1 -1
- package/dist/scrypted-main.js +3 -437
- package/dist/scrypted-main.js.map +1 -1
- package/dist/scrypted-plugin-main.js +8 -0
- package/dist/scrypted-plugin-main.js.map +1 -0
- package/dist/scrypted-server-main.js +448 -0
- package/dist/scrypted-server-main.js.map +1 -0
- package/package.json +5 -4
- package/python/media.py +1 -12
- package/python/plugin-remote.py +15 -7
- package/python/rpc.py +1 -1
- package/src/http-interfaces.ts +12 -5
- package/src/level.ts +0 -2
- package/src/plugin/media.ts +3 -3
- package/src/plugin/plugin-api.ts +9 -1
- package/src/plugin/plugin-console.ts +0 -1
- package/src/plugin/plugin-device.ts +26 -2
- package/src/plugin/plugin-host-api.ts +2 -2
- package/src/plugin/plugin-host.ts +122 -253
- package/src/plugin/plugin-http.ts +2 -2
- package/src/plugin/plugin-lazy-remote.ts +1 -1
- package/src/plugin/plugin-npm-dependencies.ts +7 -2
- package/src/plugin/plugin-remote-worker.ts +272 -0
- package/src/plugin/plugin-remote.ts +46 -17
- package/src/plugin/plugin-repl.ts +4 -2
- package/src/plugin/system.ts +15 -13
- package/src/rpc.ts +18 -3
- package/src/runtime.ts +19 -4
- package/src/scrypted-main.ts +3 -508
- package/src/scrypted-plugin-main.ts +6 -0
- package/src/scrypted-server-main.ts +516 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { RpcMessage, RpcPeer } from '../rpc';
|
|
2
|
+
import { SystemManager, DeviceManager, ScryptedNativeId } from '@scrypted/sdk/types'
|
|
3
|
+
import { attachPluginRemote, PluginReader } from './plugin-remote';
|
|
4
|
+
import { PluginAPI } from './plugin-api';
|
|
5
|
+
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'
|
|
10
|
+
import { installOptionalDependencies } from './plugin-npm-dependencies';
|
|
11
|
+
import { createREPLServer } from './plugin-repl';
|
|
12
|
+
|
|
13
|
+
export function startSharedPluginRemote() {
|
|
14
|
+
process.setMaxListeners(100);
|
|
15
|
+
process.on('message', (message: any) => {
|
|
16
|
+
if (message.type === 'start')
|
|
17
|
+
startPluginRemote(message.pluginId)
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function startPluginRemote(pluginId: string) {
|
|
22
|
+
let peerSend: (message: RpcMessage, reject?: (e: Error) => void) => void;
|
|
23
|
+
let peerListener: NodeJS.MessageListener;
|
|
24
|
+
|
|
25
|
+
if (process.argv[3] === '@scrypted/shared') {
|
|
26
|
+
peerSend = (message, reject) => process.send({
|
|
27
|
+
pluginId,
|
|
28
|
+
message,
|
|
29
|
+
}, undefined, {
|
|
30
|
+
swallowErrors: !reject,
|
|
31
|
+
}, e => {
|
|
32
|
+
if (e)
|
|
33
|
+
reject?.(e);
|
|
34
|
+
});
|
|
35
|
+
peerListener = (message: any) => {
|
|
36
|
+
if (message.type === 'message' && message.pluginId === pluginId)
|
|
37
|
+
peer.handleMessage(message.message);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
peerSend = (message, reject) => process.send(message, undefined, {
|
|
42
|
+
swallowErrors: !reject,
|
|
43
|
+
}, e => {
|
|
44
|
+
if (e)
|
|
45
|
+
reject?.(e);
|
|
46
|
+
});
|
|
47
|
+
peerListener = message => peer.handleMessage(message as RpcMessage);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const peer = new RpcPeer('unknown', 'host', peerSend);
|
|
51
|
+
peer.transportSafeArgumentTypes.add(Buffer.name);
|
|
52
|
+
process.on('message', peerListener);
|
|
53
|
+
process.on('disconnect', () => {
|
|
54
|
+
console.error('peer host disconnected, exiting.');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
let systemManager: SystemManager;
|
|
59
|
+
let deviceManager: DeviceManager;
|
|
60
|
+
let api: PluginAPI;
|
|
61
|
+
|
|
62
|
+
const getConsole = (hook: (stdout: PassThrough, stderr: PassThrough) => Promise<void>,
|
|
63
|
+
also?: Console, alsoPrefix?: string) => {
|
|
64
|
+
|
|
65
|
+
const stdout = new PassThrough();
|
|
66
|
+
const stderr = new PassThrough();
|
|
67
|
+
|
|
68
|
+
hook(stdout, stderr);
|
|
69
|
+
|
|
70
|
+
const ret = new Console(stdout, stderr);
|
|
71
|
+
|
|
72
|
+
const methods = [
|
|
73
|
+
'log', 'warn',
|
|
74
|
+
'dir', 'time',
|
|
75
|
+
'timeEnd', 'timeLog',
|
|
76
|
+
'trace', 'assert',
|
|
77
|
+
'clear', 'count',
|
|
78
|
+
'countReset', 'group',
|
|
79
|
+
'groupEnd', 'table',
|
|
80
|
+
'debug', 'info',
|
|
81
|
+
'dirxml', 'error',
|
|
82
|
+
'groupCollapsed',
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const printers = ['log', 'info', 'debug', 'trace', 'warn', 'error'];
|
|
86
|
+
for (const m of methods) {
|
|
87
|
+
const old = (ret as any)[m].bind(ret);
|
|
88
|
+
(ret as any)[m] = (...args: any[]) => {
|
|
89
|
+
// prefer the mixin version for local/remote console dump.
|
|
90
|
+
if (also && alsoPrefix && printers.includes(m)) {
|
|
91
|
+
(also as any)[m](alsoPrefix, ...args);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
(console as any)[m](...args);
|
|
95
|
+
}
|
|
96
|
+
// call through to old method to ensure it gets written
|
|
97
|
+
// to log buffer.
|
|
98
|
+
old(...args);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return ret;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let pluginsPromise: Promise<any>;
|
|
106
|
+
function getPlugins() {
|
|
107
|
+
if (!pluginsPromise)
|
|
108
|
+
pluginsPromise = api.getComponent('plugins');
|
|
109
|
+
return pluginsPromise;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const getDeviceConsole = (nativeId?: ScryptedNativeId) => {
|
|
113
|
+
// the the plugin console is simply the default console
|
|
114
|
+
// and gets read from stderr/stdout.
|
|
115
|
+
if (!nativeId)
|
|
116
|
+
return console;
|
|
117
|
+
|
|
118
|
+
return getConsole(async (stdout, stderr) => {
|
|
119
|
+
const connect = async () => {
|
|
120
|
+
const plugins = await getPlugins();
|
|
121
|
+
const port = await plugins.getRemoteServicePort(peer.selfName, 'console-writer');
|
|
122
|
+
const socket = net.connect(port);
|
|
123
|
+
socket.write(nativeId + '\n');
|
|
124
|
+
const writer = (data: Buffer) => {
|
|
125
|
+
socket.write(data);
|
|
126
|
+
};
|
|
127
|
+
stdout.on('data', writer);
|
|
128
|
+
stderr.on('data', writer);
|
|
129
|
+
socket.on('error', () => {
|
|
130
|
+
stdout.removeAllListeners();
|
|
131
|
+
stderr.removeAllListeners();
|
|
132
|
+
stdout.pause();
|
|
133
|
+
stderr.pause();
|
|
134
|
+
setTimeout(connect, 10000);
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
connect();
|
|
138
|
+
}, undefined, undefined);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const getMixinConsole = (mixinId: string, nativeId: ScryptedNativeId) => {
|
|
142
|
+
return getConsole(async (stdout, stderr) => {
|
|
143
|
+
if (!mixinId) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// todo: fix this. a mixin provider can mixin another device to make it a mixin provider itself.
|
|
147
|
+
// so the mixin id in the mixin table will be incorrect.
|
|
148
|
+
// there's no easy way to fix this from the remote.
|
|
149
|
+
// if (!systemManager.getDeviceById(mixinId).mixins.includes(idForNativeId(nativeId))) {
|
|
150
|
+
// return;
|
|
151
|
+
// }
|
|
152
|
+
const reconnect = () => {
|
|
153
|
+
stdout.removeAllListeners();
|
|
154
|
+
stderr.removeAllListeners();
|
|
155
|
+
stdout.pause();
|
|
156
|
+
stderr.pause();
|
|
157
|
+
setTimeout(tryConnect, 10000);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const connect = async () => {
|
|
161
|
+
const ds = deviceManager.getDeviceState(nativeId);
|
|
162
|
+
if (!ds) {
|
|
163
|
+
// deleted?
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const plugins = await getPlugins();
|
|
168
|
+
const { pluginId, nativeId: mixinNativeId } = await plugins.getDeviceInfo(mixinId);
|
|
169
|
+
const port = await plugins.getRemoteServicePort(pluginId, 'console-writer');
|
|
170
|
+
const socket = net.connect(port);
|
|
171
|
+
socket.write(mixinNativeId + '\n');
|
|
172
|
+
const writer = (data: Buffer) => {
|
|
173
|
+
let str = data.toString().trim();
|
|
174
|
+
str = str.replaceAll('\n', `\n[${ds.name}]: `);
|
|
175
|
+
str = `[${ds.name}]: ` + str + '\n';
|
|
176
|
+
socket.write(str);
|
|
177
|
+
};
|
|
178
|
+
stdout.on('data', writer);
|
|
179
|
+
stderr.on('data', writer);
|
|
180
|
+
socket.on('close', reconnect);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const tryConnect = async () => {
|
|
184
|
+
try {
|
|
185
|
+
await connect();
|
|
186
|
+
}
|
|
187
|
+
catch (e) {
|
|
188
|
+
reconnect();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
tryConnect();
|
|
192
|
+
}, getDeviceConsole(nativeId), `[${systemManager.getDeviceById(mixinId)?.name}]`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let lastCpuUsage: NodeJS.CpuUsage;
|
|
196
|
+
setInterval(() => {
|
|
197
|
+
const cpuUsage = process.cpuUsage(lastCpuUsage);
|
|
198
|
+
lastCpuUsage = cpuUsage;
|
|
199
|
+
peer.sendOob({
|
|
200
|
+
type: 'stats',
|
|
201
|
+
cpu: cpuUsage,
|
|
202
|
+
memoryUsage: process.memoryUsage(),
|
|
203
|
+
});
|
|
204
|
+
global?.gc();
|
|
205
|
+
}, 10000);
|
|
206
|
+
|
|
207
|
+
let replPort: Promise<number>;
|
|
208
|
+
|
|
209
|
+
let _pluginConsole: Console;
|
|
210
|
+
const getPluginConsole = () => {
|
|
211
|
+
if (!_pluginConsole)
|
|
212
|
+
_pluginConsole = getDeviceConsole(undefined);
|
|
213
|
+
return _pluginConsole;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
attachPluginRemote(peer, {
|
|
217
|
+
createMediaManager: async (sm) => {
|
|
218
|
+
systemManager = sm;
|
|
219
|
+
return new MediaManagerImpl(systemManager, getPluginConsole());
|
|
220
|
+
},
|
|
221
|
+
onGetRemote: async (_api, _pluginId) => {
|
|
222
|
+
api = _api;
|
|
223
|
+
peer.selfName = pluginId;
|
|
224
|
+
},
|
|
225
|
+
onPluginReady: async (scrypted, params, plugin) => {
|
|
226
|
+
replPort = createREPLServer(scrypted, params, plugin);
|
|
227
|
+
},
|
|
228
|
+
getPluginConsole,
|
|
229
|
+
getDeviceConsole,
|
|
230
|
+
getMixinConsole,
|
|
231
|
+
async getServicePort(name, ...args: any[]) {
|
|
232
|
+
if (name === 'repl') {
|
|
233
|
+
if (!replPort)
|
|
234
|
+
throw new Error('REPL unavailable: Plugin not loaded.')
|
|
235
|
+
return replPort;
|
|
236
|
+
}
|
|
237
|
+
throw new Error(`unknown service ${name}`);
|
|
238
|
+
},
|
|
239
|
+
async onLoadZip(pluginReader: PluginReader, packageJson: any) {
|
|
240
|
+
const entry = pluginReader('main.nodejs.js.map')
|
|
241
|
+
const map = entry?.toString();
|
|
242
|
+
|
|
243
|
+
installSourceMapSupport({
|
|
244
|
+
environment: 'node',
|
|
245
|
+
retrieveSourceMap(source) {
|
|
246
|
+
if (source === '/plugin/main.nodejs.js' || source === `/${pluginId}/main.nodejs.js`) {
|
|
247
|
+
if (!map)
|
|
248
|
+
return null;
|
|
249
|
+
return {
|
|
250
|
+
url: '/plugin/main.nodejs.js',
|
|
251
|
+
map,
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
await installOptionalDependencies(getPluginConsole(), packageJson);
|
|
258
|
+
}
|
|
259
|
+
}).then(scrypted => {
|
|
260
|
+
systemManager = scrypted.systemManager;
|
|
261
|
+
deviceManager = scrypted.deviceManager;
|
|
262
|
+
|
|
263
|
+
process.on('uncaughtException', e => {
|
|
264
|
+
getPluginConsole().error('uncaughtException', e);
|
|
265
|
+
scrypted.log.e('uncaughtException ' + e?.toString());
|
|
266
|
+
});
|
|
267
|
+
process.on('unhandledRejection', e => {
|
|
268
|
+
getPluginConsole().error('unhandledRejection', e);
|
|
269
|
+
scrypted.log.e('unhandledRejection ' + e?.toString());
|
|
270
|
+
});
|
|
271
|
+
})
|
|
272
|
+
}
|
|
@@ -7,6 +7,8 @@ import { SystemManagerImpl } from './system';
|
|
|
7
7
|
import { RpcPeer, RPCResultError, PROPERTY_PROXY_ONEWAY_METHODS, PROPERTY_JSON_DISABLE_SERIALIZATION } from '../rpc';
|
|
8
8
|
import { BufferSerializer } from './buffer-serializer';
|
|
9
9
|
import { createWebSocketClass, WebSocketConnectCallbacks, WebSocketMethods } from './plugin-remote-websocket';
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
const {link} = require('linkfs');
|
|
10
12
|
|
|
11
13
|
class DeviceLogger implements Logger {
|
|
12
14
|
nativeId: ScryptedNativeId;
|
|
@@ -306,13 +308,15 @@ export interface WebSocketCustomHandler {
|
|
|
306
308
|
methods: WebSocketMethods;
|
|
307
309
|
}
|
|
308
310
|
|
|
311
|
+
export type PluginReader = (name: string) => Buffer;
|
|
312
|
+
|
|
309
313
|
export interface PluginRemoteAttachOptions {
|
|
310
314
|
createMediaManager?: (systemManager: SystemManager) => Promise<MediaManager>;
|
|
311
315
|
getServicePort?: (name: string, ...args: any[]) => Promise<number>;
|
|
312
316
|
getDeviceConsole?: (nativeId?: ScryptedNativeId) => Console;
|
|
313
317
|
getPluginConsole?: () => Console;
|
|
314
318
|
getMixinConsole?: (id: string, nativeId?: ScryptedNativeId) => Console;
|
|
315
|
-
onLoadZip?: (
|
|
319
|
+
onLoadZip?: (pluginReader: PluginReader, packageJson: any) => Promise<void>;
|
|
316
320
|
onGetRemote?: (api: PluginAPI, pluginId: string) => Promise<void>;
|
|
317
321
|
onPluginReady?: (scrypted: ScryptedStatic, params: any, plugin: any) => Promise<void>;
|
|
318
322
|
}
|
|
@@ -434,26 +438,51 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
|
|
|
434
438
|
done(ret);
|
|
435
439
|
},
|
|
436
440
|
|
|
437
|
-
async loadZip(packageJson: any, zipData: Buffer, zipOptions?: PluginRemoteLoadZipOptions) {
|
|
441
|
+
async loadZip(packageJson: any, zipData: Buffer | string, zipOptions?: PluginRemoteLoadZipOptions) {
|
|
438
442
|
const pluginConsole = getPluginConsole?.();
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
+
|
|
444
|
+
let volume: any;
|
|
445
|
+
let pluginReader: PluginReader;
|
|
446
|
+
if (zipOptions?.unzippedPath && fs.existsSync(zipOptions?.unzippedPath)) {
|
|
447
|
+
volume = link(fs, ['', path.join(zipOptions.unzippedPath, 'fs')]);
|
|
448
|
+
pluginReader = name => {
|
|
449
|
+
const filename = path.join(zipOptions.unzippedPath, name);
|
|
450
|
+
if (!fs.existsSync(filename))
|
|
451
|
+
return;
|
|
452
|
+
return fs.readFileSync(filename);
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
const admZip = new AdmZip(zipData);
|
|
457
|
+
volume = new Volume();
|
|
458
|
+
for (const entry of admZip.getEntries()) {
|
|
459
|
+
if (entry.isDirectory)
|
|
460
|
+
continue;
|
|
461
|
+
if (!entry.entryName.startsWith('fs/'))
|
|
462
|
+
continue;
|
|
463
|
+
const name = entry.entryName.substring('fs/'.length);
|
|
464
|
+
volume.mkdirpSync(path.dirname(name));
|
|
465
|
+
const data = entry.getData();
|
|
466
|
+
volume.writeFileSync(name, data);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
pluginReader = name => {
|
|
470
|
+
const entry = admZip.getEntry(name);
|
|
471
|
+
if (!entry)
|
|
472
|
+
return;
|
|
473
|
+
return entry.getData();
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
zipData = undefined;
|
|
477
|
+
|
|
478
|
+
await options?.onLoadZip?.(pluginReader, packageJson);
|
|
479
|
+
const main = pluginReader('main.nodejs.js');
|
|
480
|
+
pluginReader = undefined;
|
|
481
|
+
const script = main.toString();
|
|
443
482
|
const window: any = {};
|
|
444
483
|
const exports: any = window;
|
|
445
484
|
window.exports = exports;
|
|
446
485
|
|
|
447
|
-
const volume = new Volume();
|
|
448
|
-
for (const entry of zip.getEntries()) {
|
|
449
|
-
if (entry.isDirectory)
|
|
450
|
-
continue;
|
|
451
|
-
if (!entry.entryName.startsWith('fs/'))
|
|
452
|
-
continue;
|
|
453
|
-
const name = entry.entryName.substr('fs/'.length);
|
|
454
|
-
volume.mkdirpSync(path.dirname(name));
|
|
455
|
-
volume.writeFileSync(name, entry.getData());
|
|
456
|
-
}
|
|
457
486
|
|
|
458
487
|
function websocketConnect(url: string, protocols: any, callbacks: WebSocketConnectCallbacks) {
|
|
459
488
|
if (url.startsWith('io://') || url.startsWith('ws://')) {
|
|
@@ -476,7 +505,7 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
|
|
|
476
505
|
exports,
|
|
477
506
|
window,
|
|
478
507
|
require: (name: string) => {
|
|
479
|
-
if (name === 'fs' && !packageJson.scrypted.realfs) {
|
|
508
|
+
if (name === 'fakefs' || (name === 'fs' && !packageJson.scrypted.realfs)) {
|
|
480
509
|
return volume;
|
|
481
510
|
}
|
|
482
511
|
if (name === 'realfs') {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { EventEmitter } from 'ws';
|
|
2
1
|
import { listenZero } from './listen-zero';
|
|
3
2
|
import { Server } from 'net';
|
|
4
3
|
import { once } from 'events';
|
|
@@ -33,8 +32,11 @@ export async function createREPLServer(scrypted: ScryptedStatic, params: any, pl
|
|
|
33
32
|
device = await device.getDevice(c);
|
|
34
33
|
}
|
|
35
34
|
|
|
35
|
+
const realDevice = systemManager.getDeviceById(device.id);
|
|
36
|
+
|
|
36
37
|
const ctx = Object.assign(params, {
|
|
37
|
-
device
|
|
38
|
+
device,
|
|
39
|
+
realDevice,
|
|
38
40
|
});
|
|
39
41
|
delete ctx.console;
|
|
40
42
|
delete ctx.window;
|
package/src/plugin/system.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EventListenerOptions, EventDetails, EventListenerRegister, ScryptedDevice, ScryptedInterface, ScryptedInterfaceDescriptors, SystemDeviceState, SystemManager, ScryptedInterfaceProperty, ScryptedDeviceType, Logger } from "@scrypted/sdk/types";
|
|
2
2
|
import { PluginAPI } from "./plugin-api";
|
|
3
|
-
import { handleFunctionInvocations, PROPERTY_PROXY_ONEWAY_METHODS } from '../rpc';
|
|
3
|
+
import { handleFunctionInvocations, PrimitiveProxyHandler, PROPERTY_PROXY_ONEWAY_METHODS } from '../rpc';
|
|
4
4
|
import { EventRegistry } from "../event-registry";
|
|
5
5
|
import { allInterfaceProperties, isValidInterfaceMethod } from "./descriptor";
|
|
6
6
|
|
|
@@ -11,13 +11,13 @@ function newDeviceProxy(id: string, systemManager: SystemManagerImpl) {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
class DeviceProxyHandler implements
|
|
15
|
-
device: ScryptedDevice
|
|
16
|
-
id: string
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
this.
|
|
14
|
+
class DeviceProxyHandler implements PrimitiveProxyHandler<any>, ScryptedDevice {
|
|
15
|
+
device: Promise<ScryptedDevice>;
|
|
16
|
+
constructor(public id: string, public systemManager: SystemManagerImpl) {
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
toPrimitive() {
|
|
20
|
+
return `ScryptedDevice-${this.id}`
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
get(target: any, p: PropertyKey, receiver: any): any {
|
|
@@ -41,19 +41,20 @@ class DeviceProxyHandler implements ProxyHandler<any>, ScryptedDevice {
|
|
|
41
41
|
return new Proxy(() => p, this);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
ensureDevice() {
|
|
45
45
|
if (!this.device)
|
|
46
|
-
this.device =
|
|
46
|
+
this.device = this.systemManager.api.getDeviceById(this.id);
|
|
47
|
+
return this.device;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
async apply(target: any, thisArg: any, argArray?: any) {
|
|
50
51
|
const method = target();
|
|
51
|
-
await this.ensureDevice();
|
|
52
|
+
const device = await this.ensureDevice();
|
|
52
53
|
if (false && method === 'refresh') {
|
|
53
54
|
const name = this.systemManager.state[this.id]?.[ScryptedInterfaceProperty.name].value;
|
|
54
55
|
this.systemManager.log.i(`requested refresh ${name}`);
|
|
55
56
|
}
|
|
56
|
-
return (
|
|
57
|
+
return (device as any)[method](...argArray);
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
listen(event: string | EventListenerOptions, callback: (eventSource: ScryptedDevice, eventDetails: EventDetails, eventData: any) => void): EventListenerRegister {
|
|
@@ -84,7 +85,8 @@ class EventListenerRegisterImpl implements EventListenerRegister {
|
|
|
84
85
|
async removeListener(): Promise<void> {
|
|
85
86
|
try {
|
|
86
87
|
const register = await this.promise;
|
|
87
|
-
|
|
88
|
+
this.promise = undefined;
|
|
89
|
+
register?.removeListener();
|
|
88
90
|
}
|
|
89
91
|
catch (e) {
|
|
90
92
|
console.error('removeListener', e);
|
package/src/rpc.ts
CHANGED
|
@@ -63,7 +63,7 @@ interface Deferred {
|
|
|
63
63
|
reject: any;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
export function handleFunctionInvocations(thiz:
|
|
66
|
+
export function handleFunctionInvocations(thiz: PrimitiveProxyHandler<any>, target: any, p: PropertyKey, receiver: any): any {
|
|
67
67
|
if (p === 'apply') {
|
|
68
68
|
return (thisArg: any, args: any[]) => {
|
|
69
69
|
return thiz.apply(target, thiz, args);
|
|
@@ -74,6 +74,11 @@ export function handleFunctionInvocations(thiz: ProxyHandler<any>, target: any,
|
|
|
74
74
|
return thiz.apply(target, thiz, args);
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
+
else if (p === 'toString' || p === Symbol.toPrimitive) {
|
|
78
|
+
return (thisArg: any, ...args: any[]) => {
|
|
79
|
+
return thiz.toPrimitive();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
export const PROPERTY_PROXY_ONEWAY_METHODS = '__proxy_oneway_methods';
|
|
@@ -81,8 +86,11 @@ export const PROPERTY_JSON_DISABLE_SERIALIZATION = '__json_disable_serialization
|
|
|
81
86
|
export const PROPERTY_PROXY_PROPERTIES = '__proxy_props';
|
|
82
87
|
export const PROPERTY_JSON_COPY_SERIALIZE_CHILDREN = '__json_copy_serialize_children';
|
|
83
88
|
|
|
84
|
-
|
|
89
|
+
export interface PrimitiveProxyHandler<T extends object> extends ProxyHandler<T> {
|
|
90
|
+
toPrimitive(): any;
|
|
91
|
+
}
|
|
85
92
|
|
|
93
|
+
class RpcProxy implements PrimitiveProxyHandler<any> {
|
|
86
94
|
constructor(public peer: RpcPeer,
|
|
87
95
|
public entry: LocalProxiedEntry,
|
|
88
96
|
public constructorName: string,
|
|
@@ -90,6 +98,11 @@ class RpcProxy implements ProxyHandler<any> {
|
|
|
90
98
|
public proxyOneWayMethods: string[]) {
|
|
91
99
|
}
|
|
92
100
|
|
|
101
|
+
toPrimitive() {
|
|
102
|
+
const peer = this.peer;
|
|
103
|
+
return `RpcProxy-${peer.selfName}:${peer.peerName}: ${this.constructorName}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
93
106
|
get(target: any, p: PropertyKey, receiver: any): any {
|
|
94
107
|
if (p === '__proxy_id')
|
|
95
108
|
return this.entry.id;
|
|
@@ -122,6 +135,9 @@ class RpcProxy implements ProxyHandler<any> {
|
|
|
122
135
|
}
|
|
123
136
|
|
|
124
137
|
apply(target: any, thisArg: any, argArray?: any): any {
|
|
138
|
+
if (Object.isFrozen(this.peer.pendingResults))
|
|
139
|
+
return Promise.reject(new RPCResultError(this.peer, 'RpcPeer has been killed'));
|
|
140
|
+
|
|
125
141
|
// rpc objects can be functions. if the function is a oneway method,
|
|
126
142
|
// it will have a null in the oneway method list. this is because
|
|
127
143
|
// undefined is not JSON serializable.
|
|
@@ -496,7 +512,6 @@ export class RpcPeer {
|
|
|
496
512
|
const localProxiedEntry = this.localProxied.get(local);
|
|
497
513
|
// if a finalizer id is specified, it must match.
|
|
498
514
|
if (rpcFinalize.__local_proxy_finalizer_id && rpcFinalize.__local_proxy_finalizer_id !== localProxiedEntry?.finalizerId) {
|
|
499
|
-
console.error(this.selfName, this.peerName, 'finalizer mismatch')
|
|
500
515
|
break;
|
|
501
516
|
}
|
|
502
517
|
delete this.localProxyMap[rpcFinalize.__local_proxy_id];
|
package/src/runtime.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { spawn as ptySpawn } from 'node-pty';
|
|
|
30
30
|
import rimraf from 'rimraf';
|
|
31
31
|
import { getPluginVolume } from './plugin/plugin-volume';
|
|
32
32
|
import { PluginHttp } from './plugin/plugin-http';
|
|
33
|
+
import AdmZip from 'adm-zip';
|
|
33
34
|
|
|
34
35
|
interface DeviceProxyPair {
|
|
35
36
|
handler: PluginDeviceProxyHandler;
|
|
@@ -312,7 +313,7 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
|
|
|
312
313
|
}
|
|
313
314
|
}
|
|
314
315
|
|
|
315
|
-
|
|
316
|
+
handleEngineIOEndpoint(req: Request, res: ServerResponse, endpointRequest: HttpRequest, pluginData: HttpPluginData) {
|
|
316
317
|
const { pluginHost, pluginDevice } = pluginData;
|
|
317
318
|
|
|
318
319
|
(req as any).scrypted = {
|
|
@@ -325,7 +326,7 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
|
|
|
325
326
|
pluginHost.io.handleRequest(req, res);
|
|
326
327
|
}
|
|
327
328
|
|
|
328
|
-
|
|
329
|
+
handleRequestEndpoint(req: Request, res: Response, endpointRequest: HttpRequest, pluginData: HttpPluginData) {
|
|
329
330
|
const { pluginHost, pluginDevice } = pluginData;
|
|
330
331
|
const handler = this.getDevice<HttpRequestHandler>(pluginDevice._id);
|
|
331
332
|
if (handler.interfaces.includes(ScryptedInterface.EngineIOHandler) && req.headers.connection === 'upgrade' && req.headers.upgrade?.toLowerCase() === 'websocket') {
|
|
@@ -333,7 +334,7 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
|
|
|
333
334
|
console.log(ws);
|
|
334
335
|
});
|
|
335
336
|
}
|
|
336
|
-
handler.onRequest(endpointRequest, createResponseInterface(res, pluginHost.
|
|
337
|
+
handler.onRequest(endpointRequest, createResponseInterface(res, pluginHost.unzippedPath));
|
|
337
338
|
}
|
|
338
339
|
|
|
339
340
|
killPlugin(plugin: Plugin) {
|
|
@@ -459,7 +460,21 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
|
|
|
459
460
|
}
|
|
460
461
|
|
|
461
462
|
async installPlugin(plugin: Plugin, pluginDebug?: PluginDebug): Promise<PluginHost> {
|
|
462
|
-
|
|
463
|
+
const device: Device = Object.assign({}, plugin.packageJson.scrypted);
|
|
464
|
+
try {
|
|
465
|
+
if (!device.interfaces.includes(ScryptedInterface.Readme)) {
|
|
466
|
+
const zipData = Buffer.from(plugin.zip, 'base64');
|
|
467
|
+
const adm = new AdmZip(zipData);
|
|
468
|
+
const entry = adm.getEntry('README.md');
|
|
469
|
+
if (entry) {
|
|
470
|
+
device.interfaces = device.interfaces.slice();
|
|
471
|
+
device.interfaces.push(ScryptedInterface.Readme);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
catch (e) {
|
|
476
|
+
}
|
|
477
|
+
await this.upsertDevice(plugin._id, device);
|
|
463
478
|
return this.runPlugin(plugin, pluginDebug);
|
|
464
479
|
}
|
|
465
480
|
|