@scrypted/server 0.0.81 → 0.0.85
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/plugin/plugin-console.js +93 -0
- package/dist/plugin/plugin-console.js.map +1 -0
- package/dist/plugin/plugin-device.js +6 -1
- package/dist/plugin/plugin-device.js.map +1 -1
- package/dist/plugin/plugin-host-api.js +1 -1
- package/dist/plugin/plugin-host-api.js.map +1 -1
- package/dist/plugin/plugin-host.js +74 -174
- package/dist/plugin/plugin-host.js.map +1 -1
- package/dist/plugin/plugin-npm-dependencies.js +2 -2
- package/dist/plugin/plugin-npm-dependencies.js.map +1 -1
- package/dist/plugin/plugin-remote.js +7 -9
- package/dist/plugin/plugin-remote.js.map +1 -1
- package/dist/plugin/plugin-repl.js +69 -0
- package/dist/plugin/plugin-repl.js.map +1 -0
- package/dist/runtime.js +2 -21
- package/dist/runtime.js.map +1 -1
- package/dist/scrypted-main.js +33 -20
- package/dist/scrypted-main.js.map +1 -1
- package/dist/services/plugin.js +8 -0
- package/dist/services/plugin.js.map +1 -1
- package/package.json +2 -2
- package/python/plugin-remote.py +45 -20
- package/python/rpc.py +1 -1
- package/src/plugin/plugin-api.ts +2 -2
- package/src/plugin/plugin-console.ts +115 -0
- package/src/plugin/plugin-device.ts +7 -1
- package/src/plugin/plugin-host-api.ts +1 -1
- package/src/plugin/plugin-host.ts +72 -198
- package/src/plugin/plugin-npm-dependencies.ts +2 -3
- package/src/plugin/plugin-remote.ts +11 -10
- package/src/plugin/plugin-repl.ts +74 -0
- package/src/runtime.ts +2 -24
- package/src/scrypted-main.ts +36 -22
- package/src/services/plugin.ts +8 -0
- package/test/test-cert.json +4 -0
|
@@ -5,15 +5,11 @@ import { ScryptedRuntime } from '../runtime';
|
|
|
5
5
|
import { Plugin } from '../db-types';
|
|
6
6
|
import io from 'engine.io';
|
|
7
7
|
import { attachPluginRemote, setupPluginRemote } from './plugin-remote';
|
|
8
|
-
import { PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
|
|
8
|
+
import { PluginAPI, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
|
|
9
9
|
import { Logger } from '../logger';
|
|
10
10
|
import { MediaManagerHostImpl, MediaManagerImpl } from './media';
|
|
11
11
|
import { getState } from '../state';
|
|
12
12
|
import WebSocket, { EventEmitter } from 'ws';
|
|
13
|
-
import { listenZero } from './listen-zero';
|
|
14
|
-
import { Server } from 'net';
|
|
15
|
-
import repl from 'repl';
|
|
16
|
-
import { once } from 'events';
|
|
17
13
|
import { PassThrough } from 'stream';
|
|
18
14
|
import { Console } from 'console'
|
|
19
15
|
import { sleep } from '../sleep';
|
|
@@ -27,6 +23,8 @@ import readline from 'readline';
|
|
|
27
23
|
import { Readable, Writable } from 'stream';
|
|
28
24
|
import { ensurePluginVolume } from './plugin-volume';
|
|
29
25
|
import { installOptionalDependencies } from './plugin-npm-dependencies';
|
|
26
|
+
import { ConsoleServer, createConsoleServer } from './plugin-console';
|
|
27
|
+
import { createREPLServer } from './plugin-repl';
|
|
30
28
|
|
|
31
29
|
export class PluginHost {
|
|
32
30
|
worker: child_process.ChildProcess;
|
|
@@ -49,6 +47,7 @@ export class PluginHost {
|
|
|
49
47
|
memoryUsage: NodeJS.MemoryUsage,
|
|
50
48
|
};
|
|
51
49
|
killed = false;
|
|
50
|
+
consoleServer: Promise<ConsoleServer>;
|
|
52
51
|
|
|
53
52
|
kill() {
|
|
54
53
|
this.killed = true;
|
|
@@ -73,6 +72,14 @@ export class PluginHost {
|
|
|
73
72
|
}
|
|
74
73
|
}
|
|
75
74
|
}
|
|
75
|
+
|
|
76
|
+
this.consoleServer?.then(server => {
|
|
77
|
+
server.readServer.close();
|
|
78
|
+
server.writeServer.close();
|
|
79
|
+
for (const s of server.sockets) {
|
|
80
|
+
s.destroy();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
76
83
|
setTimeout(() => this.peer.kill('plugin killed'), 500);
|
|
77
84
|
}
|
|
78
85
|
|
|
@@ -84,6 +91,7 @@ export class PluginHost {
|
|
|
84
91
|
async upsertDevice(upsert: Device) {
|
|
85
92
|
const pi = await this.scrypted.upsertDevice(this.pluginId, upsert, true);
|
|
86
93
|
await this.remote.setNativeId(pi.nativeId, pi._id, pi.storage || {});
|
|
94
|
+
return pi._id;
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
constructor(scrypted: ScryptedRuntime, plugin: Plugin, public pluginDebug?: PluginDebug) {
|
|
@@ -96,7 +104,7 @@ export class PluginHost {
|
|
|
96
104
|
const volume = path.join(process.cwd(), 'volume');
|
|
97
105
|
const cwd = ensurePluginVolume(this.pluginId);
|
|
98
106
|
|
|
99
|
-
this.
|
|
107
|
+
this.startPluginHost(logger, {
|
|
100
108
|
NODE_PATH: path.join(cwd, 'node_modules'),
|
|
101
109
|
SCRYPTED_PLUGIN_VOLUME: cwd,
|
|
102
110
|
}, plugin.packageJson.scrypted.runtime);
|
|
@@ -207,7 +215,7 @@ export class PluginHost {
|
|
|
207
215
|
});
|
|
208
216
|
}
|
|
209
217
|
|
|
210
|
-
|
|
218
|
+
startPluginHost(logger: Logger, env?: any, runtime?: string) {
|
|
211
219
|
let connected = true;
|
|
212
220
|
|
|
213
221
|
if (runtime === 'python') {
|
|
@@ -227,9 +235,9 @@ export class PluginHost {
|
|
|
227
235
|
path.join(__dirname, '../../python', 'plugin-remote.py'),
|
|
228
236
|
)
|
|
229
237
|
|
|
230
|
-
this.worker = child_process.spawn('
|
|
238
|
+
this.worker = child_process.spawn('python3', args, {
|
|
231
239
|
// stdin, stdout, stderr, peer in, peer out
|
|
232
|
-
stdio: ['pipe', '
|
|
240
|
+
stdio: ['pipe', 'pipe', 'pipe', 'pipe', 'pipe'],
|
|
233
241
|
env: Object.assign({}, process.env, env),
|
|
234
242
|
});
|
|
235
243
|
|
|
@@ -263,7 +271,7 @@ export class PluginHost {
|
|
|
263
271
|
}
|
|
264
272
|
|
|
265
273
|
this.worker = child_process.fork(require.main.filename, ['child'], {
|
|
266
|
-
stdio: ['pipe', '
|
|
274
|
+
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
|
|
267
275
|
env: Object.assign({}, process.env, env),
|
|
268
276
|
serialization: 'advanced',
|
|
269
277
|
execArgv,
|
|
@@ -285,8 +293,9 @@ export class PluginHost {
|
|
|
285
293
|
this.worker.on('message', message => this.peer.handleMessage(message as any));
|
|
286
294
|
}
|
|
287
295
|
|
|
288
|
-
|
|
289
|
-
|
|
296
|
+
this.worker.stdout.on('data', data => console.log(data.toString()));
|
|
297
|
+
this.worker.stderr.on('data', data => console.error(data.toString()));
|
|
298
|
+
this.consoleServer = createConsoleServer(this.worker.stdout, this.worker.stderr);
|
|
290
299
|
|
|
291
300
|
this.worker.on('disconnect', () => {
|
|
292
301
|
connected = false;
|
|
@@ -309,171 +318,20 @@ export class PluginHost {
|
|
|
309
318
|
}
|
|
310
319
|
}
|
|
311
320
|
|
|
312
|
-
|
|
313
|
-
const outputs = new Map<string, Buffer[]>();
|
|
314
|
-
const appendOutput = (data: Buffer, nativeId: ScryptedNativeId) => {
|
|
315
|
-
if (!nativeId)
|
|
316
|
-
nativeId = undefined;
|
|
317
|
-
let buffers = outputs.get(nativeId);
|
|
318
|
-
if (!buffers) {
|
|
319
|
-
buffers = [];
|
|
320
|
-
outputs.set(nativeId, buffers);
|
|
321
|
-
}
|
|
322
|
-
buffers.push(data);
|
|
323
|
-
// when we're over 4000 lines or whatever these buffer are,
|
|
324
|
-
// truncate down to 2000.
|
|
325
|
-
if (buffers.length > 4000)
|
|
326
|
-
outputs.set(nativeId, buffers.slice(buffers.length - 2000))
|
|
327
|
-
};
|
|
328
|
-
events.on('stdout', appendOutput);
|
|
329
|
-
events.on('stderr', appendOutput);
|
|
330
|
-
|
|
331
|
-
const server = new Server(async (socket) => {
|
|
332
|
-
let [filter] = await once(socket, 'data');
|
|
333
|
-
filter = filter.toString().trim();
|
|
334
|
-
if (filter === 'undefined')
|
|
335
|
-
filter = undefined;
|
|
336
|
-
|
|
337
|
-
const buffers = outputs.get(filter);
|
|
338
|
-
if (buffers) {
|
|
339
|
-
const concat = Buffer.concat(buffers);
|
|
340
|
-
outputs.set(filter, [concat]);
|
|
341
|
-
socket.write(concat);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const cb = (data: Buffer, nativeId: ScryptedNativeId) => {
|
|
345
|
-
if (nativeId !== filter)
|
|
346
|
-
return;
|
|
347
|
-
socket.write(data);
|
|
348
|
-
};
|
|
349
|
-
events.on('stdout', cb)
|
|
350
|
-
events.on('stderr', cb)
|
|
351
|
-
|
|
352
|
-
const cleanup = () => {
|
|
353
|
-
events.removeListener('stdout', cb);
|
|
354
|
-
events.removeListener('stderr', cb);
|
|
355
|
-
};
|
|
356
|
-
|
|
357
|
-
socket.on('close', cleanup);
|
|
358
|
-
socket.on('error', cleanup);
|
|
359
|
-
socket.on('end', cleanup);
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const writeServer = new Server(async (socket) => {
|
|
364
|
-
const [data] = await once(socket, 'data');
|
|
365
|
-
let filter: string = data.toString();
|
|
366
|
-
const newline = filter.indexOf('\n');
|
|
367
|
-
if (newline !== -1) {
|
|
368
|
-
socket.unshift(Buffer.from(filter.substring(newline + 1)));
|
|
369
|
-
}
|
|
370
|
-
filter = filter.substring(0, newline);
|
|
371
|
-
|
|
372
|
-
if (filter === 'undefined')
|
|
373
|
-
filter = undefined;
|
|
374
|
-
|
|
375
|
-
const cb = (data: Buffer) => events.emit('stdout', data, filter);
|
|
376
|
-
|
|
377
|
-
socket.on('data', cb);
|
|
378
|
-
|
|
379
|
-
const cleanup = () => {
|
|
380
|
-
events.removeListener('data', cb);
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
socket.on('close', cleanup);
|
|
384
|
-
socket.on('error', cleanup);
|
|
385
|
-
socket.on('end', cleanup);
|
|
386
|
-
});
|
|
387
|
-
const consoleReader = await listenZero(server);
|
|
388
|
-
const consoleWriter = await listenZero(writeServer);
|
|
389
|
-
|
|
390
|
-
return [consoleReader, consoleWriter];
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
async function createREPLServer(events: EventEmitter): Promise<number> {
|
|
394
|
-
const [[scrypted], [params], [plugin]] = await Promise.all([once(events, 'scrypted'), once(events, 'params'), once(events, 'plugin')]);
|
|
395
|
-
const { deviceManager, systemManager } = scrypted;
|
|
396
|
-
const server = new Server(async (socket) => {
|
|
397
|
-
let [filter] = await once(socket, 'data');
|
|
398
|
-
filter = filter.toString().trim();
|
|
399
|
-
if (filter === 'undefined')
|
|
400
|
-
filter = undefined;
|
|
401
|
-
|
|
402
|
-
const chain: string[] = [];
|
|
403
|
-
const nativeIds: Map<string, any> = deviceManager.nativeIds;
|
|
404
|
-
const reversed = new Map<string, string>();
|
|
405
|
-
for (const nativeId of nativeIds.keys()) {
|
|
406
|
-
reversed.set(nativeIds.get(nativeId).id, nativeId);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
while (filter) {
|
|
410
|
-
const { id } = nativeIds.get(filter);
|
|
411
|
-
const d = await systemManager.getDeviceById(id);
|
|
412
|
-
chain.push(filter);
|
|
413
|
-
filter = reversed.get(d.providerId);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
chain.reverse();
|
|
417
|
-
let device = plugin;
|
|
418
|
-
for (const c of chain) {
|
|
419
|
-
device = await device.getDevice(c);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const ctx = Object.assign(params, {
|
|
424
|
-
device
|
|
425
|
-
});
|
|
426
|
-
delete ctx.console;
|
|
427
|
-
delete ctx.window;
|
|
428
|
-
delete ctx.WebSocket;
|
|
429
|
-
delete ctx.pluginHostAPI;
|
|
430
|
-
|
|
431
|
-
const replFilter = new Set<string>(['require', 'localStorage'])
|
|
432
|
-
const replVariables = Object.keys(ctx).filter(key => !replFilter.has(key));
|
|
433
|
-
|
|
434
|
-
const welcome = `JavaScript REPL variables:\n${replVariables.map(key => ' ' + key).join('\n')}\n\n`;
|
|
435
|
-
socket.write(welcome);
|
|
436
|
-
|
|
437
|
-
const r = repl.start({
|
|
438
|
-
terminal: true,
|
|
439
|
-
input: socket,
|
|
440
|
-
output: socket,
|
|
441
|
-
// writer(this: REPLServer, obj: any) {
|
|
442
|
-
// const ret = util.inspect(obj, {
|
|
443
|
-
// colors: true,
|
|
444
|
-
// });
|
|
445
|
-
// return ret;//.replaceAll('\n', '\r\n');
|
|
446
|
-
// },
|
|
447
|
-
preview: false,
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
Object.assign(r.context, ctx);
|
|
451
|
-
|
|
452
|
-
const cleanup = () => {
|
|
453
|
-
r.close();
|
|
454
|
-
};
|
|
455
|
-
|
|
456
|
-
socket.on('close', cleanup);
|
|
457
|
-
socket.on('error', cleanup);
|
|
458
|
-
socket.on('end', cleanup);
|
|
459
|
-
});
|
|
460
|
-
return listenZero(server);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
export function startPluginClusterWorker() {
|
|
321
|
+
export function startPluginRemote() {
|
|
464
322
|
const peer = new RpcPeer('unknown', 'host', (message, reject) => process.send(message, undefined, {
|
|
465
323
|
swallowErrors: !reject,
|
|
466
324
|
}, e => {
|
|
467
325
|
if (e)
|
|
468
|
-
reject(e);
|
|
326
|
+
reject?.(e);
|
|
469
327
|
}));
|
|
470
328
|
peer.transportSafeArgumentTypes.add(Buffer.name);
|
|
471
329
|
process.on('message', message => peer.handleMessage(message as RpcMessage));
|
|
472
330
|
|
|
473
|
-
const events = new EventEmitter();
|
|
474
|
-
|
|
475
331
|
let systemManager: SystemManager;
|
|
476
332
|
let deviceManager: DeviceManager;
|
|
333
|
+
let api: PluginAPI;
|
|
334
|
+
let pluginId: string;
|
|
477
335
|
|
|
478
336
|
function idForNativeId(nativeId: ScryptedNativeId) {
|
|
479
337
|
if (!deviceManager)
|
|
@@ -527,16 +385,31 @@ export function startPluginClusterWorker() {
|
|
|
527
385
|
|
|
528
386
|
const getDeviceConsole = (nativeId?: ScryptedNativeId) => {
|
|
529
387
|
return getConsole(async (stdout, stderr) => {
|
|
530
|
-
|
|
531
|
-
|
|
388
|
+
const plugins = await api.getComponent('plugins');
|
|
389
|
+
const connect = async () => {
|
|
390
|
+
const port = await plugins.getRemoteServicePort(peer.selfName, 'console-writer');
|
|
391
|
+
const socket = net.connect(port);
|
|
392
|
+
socket.write(nativeId + '\n');
|
|
393
|
+
const writer = (data: Buffer) => {
|
|
394
|
+
socket.write(data);
|
|
395
|
+
};
|
|
396
|
+
stdout.on('data', writer);
|
|
397
|
+
stderr.on('data', writer);
|
|
398
|
+
socket.on('error', () => {
|
|
399
|
+
stdout.removeAllListeners();
|
|
400
|
+
stderr.removeAllListeners();
|
|
401
|
+
stdout.pause();
|
|
402
|
+
stderr.pause();
|
|
403
|
+
setTimeout(connect, 10000);
|
|
404
|
+
});
|
|
405
|
+
};
|
|
406
|
+
connect();
|
|
532
407
|
}, undefined, undefined);
|
|
533
408
|
}
|
|
534
409
|
|
|
535
410
|
const getMixinConsole = (mixinId: string, nativeId?: ScryptedNativeId) => {
|
|
536
411
|
return getConsole(async (stdout, stderr) => {
|
|
537
412
|
if (!mixinId || !systemManager.getDeviceById(mixinId).mixins.includes(idForNativeId(nativeId))) {
|
|
538
|
-
stdout.on('data', data => events.emit('stdout', data, nativeId));
|
|
539
|
-
stderr.on('data', data => events.emit('stderr', data, nativeId));
|
|
540
413
|
return;
|
|
541
414
|
}
|
|
542
415
|
const plugins = await systemManager.getComponent('plugins');
|
|
@@ -582,28 +455,40 @@ export function startPluginClusterWorker() {
|
|
|
582
455
|
global?.gc();
|
|
583
456
|
}, 10000);
|
|
584
457
|
|
|
585
|
-
|
|
586
|
-
const replPort = createREPLServer(events);
|
|
458
|
+
let replPort: Promise<number>;
|
|
587
459
|
|
|
588
|
-
|
|
460
|
+
let _pluginConsole: Console;
|
|
461
|
+
const getPluginConsole = () => {
|
|
462
|
+
if (_pluginConsole)
|
|
463
|
+
return _pluginConsole;
|
|
464
|
+
_pluginConsole = getDeviceConsole(undefined);
|
|
465
|
+
}
|
|
589
466
|
|
|
590
467
|
attachPluginRemote(peer, {
|
|
591
|
-
createMediaManager: async (
|
|
592
|
-
|
|
468
|
+
createMediaManager: async (sm) => {
|
|
469
|
+
systemManager = sm;
|
|
470
|
+
return new MediaManagerImpl(systemManager, getPluginConsole());
|
|
471
|
+
},
|
|
472
|
+
onGetRemote: async (_api, _pluginId) => {
|
|
473
|
+
api = _api;
|
|
474
|
+
pluginId = _pluginId;
|
|
475
|
+
peer.selfName = pluginId;
|
|
476
|
+
},
|
|
477
|
+
onPluginReady: async(scrypted, params, plugin) => {
|
|
478
|
+
replPort = createREPLServer(scrypted, params, plugin);
|
|
479
|
+
},
|
|
480
|
+
getPluginConsole,
|
|
593
481
|
getDeviceConsole,
|
|
594
482
|
getMixinConsole,
|
|
595
483
|
async getServicePort(name) {
|
|
596
|
-
if (name === 'repl')
|
|
484
|
+
if (name === 'repl') {
|
|
485
|
+
if (!replPort)
|
|
486
|
+
throw new Error('REPL unavailable: Plugin not loaded.')
|
|
597
487
|
return replPort;
|
|
598
|
-
|
|
599
|
-
return (await consolePorts)[0];
|
|
600
|
-
if (name === 'console-writer')
|
|
601
|
-
return (await consolePorts)[1];
|
|
488
|
+
}
|
|
602
489
|
throw new Error(`unknown service ${name}`);
|
|
603
490
|
},
|
|
604
|
-
async
|
|
605
|
-
const pluginId = packageJson.name;
|
|
606
|
-
peer.selfName = pluginId;
|
|
491
|
+
async onLoadZip(zip: AdmZip, packageJson: any) {
|
|
607
492
|
installSourceMapSupport({
|
|
608
493
|
environment: 'node',
|
|
609
494
|
retrieveSourceMap(source) {
|
|
@@ -620,29 +505,18 @@ export function startPluginClusterWorker() {
|
|
|
620
505
|
return null;
|
|
621
506
|
}
|
|
622
507
|
});
|
|
623
|
-
|
|
624
|
-
const writer = cp[1];
|
|
625
|
-
const socket = net.connect(writer);
|
|
626
|
-
await once(socket, 'connect');
|
|
627
|
-
try {
|
|
628
|
-
await installOptionalDependencies(pluginConsole, socket, packageJson);
|
|
629
|
-
}
|
|
630
|
-
finally {
|
|
631
|
-
socket.destroy();
|
|
632
|
-
}
|
|
508
|
+
await installOptionalDependencies(getPluginConsole(), packageJson);
|
|
633
509
|
}
|
|
634
510
|
}).then(scrypted => {
|
|
635
511
|
systemManager = scrypted.systemManager;
|
|
636
512
|
deviceManager = scrypted.deviceManager;
|
|
637
513
|
|
|
638
|
-
events.emit('scrypted', scrypted);
|
|
639
|
-
|
|
640
514
|
process.on('uncaughtException', e => {
|
|
641
|
-
|
|
515
|
+
getPluginConsole().error('uncaughtException', e);
|
|
642
516
|
scrypted.log.e('uncaughtException ' + e?.toString());
|
|
643
517
|
});
|
|
644
518
|
process.on('unhandledRejection', e => {
|
|
645
|
-
|
|
519
|
+
getPluginConsole().error('unhandledRejection', e);
|
|
646
520
|
scrypted.log.e('unhandledRejection ' + e?.toString());
|
|
647
521
|
});
|
|
648
522
|
})
|
|
@@ -3,9 +3,8 @@ import fs from 'fs';
|
|
|
3
3
|
import child_process from 'child_process';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { once } from 'events';
|
|
6
|
-
import { Socket } from "net";
|
|
7
6
|
|
|
8
|
-
export async function installOptionalDependencies(console: Console,
|
|
7
|
+
export async function installOptionalDependencies(console: Console, packageJson: any) {
|
|
9
8
|
const pluginVolume = ensurePluginVolume(packageJson.name);
|
|
10
9
|
const optPj = path.join(pluginVolume, 'package.json');
|
|
11
10
|
|
|
@@ -40,7 +39,7 @@ export async function installOptionalDependencies(console: Console, socket: Sock
|
|
|
40
39
|
|
|
41
40
|
const cp = child_process.spawn('npm', ['--prefix', pluginVolume, 'install'], {
|
|
42
41
|
cwd: pluginVolume,
|
|
43
|
-
stdio:
|
|
42
|
+
stdio: 'inherit',
|
|
44
43
|
});
|
|
45
44
|
|
|
46
45
|
await once(cp, 'exit');
|
|
@@ -135,7 +135,6 @@ class DeviceManagerImpl implements DeviceManager {
|
|
|
135
135
|
nativeIds = new Map<string, DeviceManagerDevice>();
|
|
136
136
|
|
|
137
137
|
constructor(public systemManager: SystemManagerImpl,
|
|
138
|
-
public events?: EventEmitter,
|
|
139
138
|
public getDeviceConsole?: (nativeId?: ScryptedNativeId) => Console,
|
|
140
139
|
public getMixinConsole?: (mixinId: string, nativeId?: ScryptedNativeId) => Console) {
|
|
141
140
|
}
|
|
@@ -283,13 +282,15 @@ export interface PluginRemoteAttachOptions {
|
|
|
283
282
|
createMediaManager?: (systemManager: SystemManager) => Promise<MediaManager>;
|
|
284
283
|
getServicePort?: (name: string) => Promise<number>;
|
|
285
284
|
getDeviceConsole?: (nativeId?: ScryptedNativeId) => Console;
|
|
285
|
+
getPluginConsole?: () => Console;
|
|
286
286
|
getMixinConsole?: (id: string, nativeId?: ScryptedNativeId) => Console;
|
|
287
|
-
|
|
288
|
-
|
|
287
|
+
onLoadZip?: (zip: AdmZip, packageJson: any) => Promise<void>;
|
|
288
|
+
onGetRemote?: (api: PluginAPI, pluginId: string) => Promise<void>;
|
|
289
|
+
onPluginReady?: (scrypted: ScryptedStatic, params: any, plugin: any) => Promise<void>;
|
|
289
290
|
}
|
|
290
291
|
|
|
291
292
|
export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOptions): Promise<ScryptedStatic> {
|
|
292
|
-
const { createMediaManager, getServicePort,
|
|
293
|
+
const { createMediaManager, getServicePort, getDeviceConsole, getMixinConsole, getPluginConsole } = options || {};
|
|
293
294
|
|
|
294
295
|
peer.addSerializer(Buffer, 'Buffer', new BufferSerializer());
|
|
295
296
|
|
|
@@ -297,8 +298,10 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
|
|
|
297
298
|
const retPromise = new Promise<ScryptedStatic>(resolve => done = resolve);
|
|
298
299
|
|
|
299
300
|
peer.params.getRemote = async (api: PluginAPI, pluginId: string) => {
|
|
301
|
+
await options?.onGetRemote?.(api, pluginId);
|
|
302
|
+
|
|
300
303
|
const systemManager = new SystemManagerImpl();
|
|
301
|
-
const deviceManager = new DeviceManagerImpl(systemManager,
|
|
304
|
+
const deviceManager = new DeviceManagerImpl(systemManager, getDeviceConsole, getMixinConsole);
|
|
302
305
|
const endpointManager = new EndpointManagerImpl();
|
|
303
306
|
const ioSockets: { [id: string]: WebSocketCallbacks } = {};
|
|
304
307
|
const mediaManager = await api.getMediaManager() || await createMediaManager(systemManager);
|
|
@@ -403,10 +406,10 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
|
|
|
403
406
|
},
|
|
404
407
|
|
|
405
408
|
async loadZip(packageJson: any, zipData: Buffer, zipOptions?: PluginRemoteLoadZipOptions) {
|
|
406
|
-
const pluginConsole =
|
|
409
|
+
const pluginConsole = getPluginConsole?.();
|
|
407
410
|
pluginConsole?.log('starting plugin', pluginId, packageJson.version);
|
|
408
411
|
const zip = new AdmZip(zipData);
|
|
409
|
-
await options?.
|
|
412
|
+
await options?.onLoadZip?.(zip, packageJson);
|
|
410
413
|
const main = zip.getEntry('main.nodejs.js');
|
|
411
414
|
const script = main.getData().toString();
|
|
412
415
|
const window: any = {};
|
|
@@ -482,12 +485,10 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
|
|
|
482
485
|
|
|
483
486
|
params.console = pluginConsole;
|
|
484
487
|
|
|
485
|
-
events?.emit('params', params);
|
|
486
|
-
|
|
487
488
|
try {
|
|
488
489
|
peer.evalLocal(script, zipOptions?.filename || '/plugin/main.nodejs.js', params);
|
|
489
|
-
events?.emit('plugin', exports.default);
|
|
490
490
|
pluginConsole?.log('plugin successfully loaded');
|
|
491
|
+
await options?.onPluginReady?.(ret, params, exports.default);
|
|
491
492
|
return exports.default;
|
|
492
493
|
}
|
|
493
494
|
catch (e) {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { EventEmitter } from 'ws';
|
|
2
|
+
import { listenZero } from './listen-zero';
|
|
3
|
+
import { Server } from 'net';
|
|
4
|
+
import { once } from 'events';
|
|
5
|
+
import repl from 'repl';
|
|
6
|
+
import { ScryptedStatic } from '@scrypted/sdk';
|
|
7
|
+
|
|
8
|
+
export async function createREPLServer(scrypted: ScryptedStatic, params: any, plugin: any): Promise<number> {
|
|
9
|
+
const { deviceManager, systemManager } = scrypted;
|
|
10
|
+
const server = new Server(async (socket) => {
|
|
11
|
+
let [filter] = await once(socket, 'data');
|
|
12
|
+
filter = filter.toString().trim();
|
|
13
|
+
if (filter === 'undefined')
|
|
14
|
+
filter = undefined;
|
|
15
|
+
|
|
16
|
+
const chain: string[] = [];
|
|
17
|
+
const nativeIds: Map<string, any> = (deviceManager as any).nativeIds;
|
|
18
|
+
const reversed = new Map<string, string>();
|
|
19
|
+
for (const nativeId of nativeIds.keys()) {
|
|
20
|
+
reversed.set(nativeIds.get(nativeId).id, nativeId);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
while (filter) {
|
|
24
|
+
const { id } = nativeIds.get(filter);
|
|
25
|
+
const d = await systemManager.getDeviceById(id);
|
|
26
|
+
chain.push(filter);
|
|
27
|
+
filter = reversed.get(d.providerId);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
chain.reverse();
|
|
31
|
+
let device = plugin;
|
|
32
|
+
for (const c of chain) {
|
|
33
|
+
device = await device.getDevice(c);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const ctx = Object.assign(params, {
|
|
37
|
+
device
|
|
38
|
+
});
|
|
39
|
+
delete ctx.console;
|
|
40
|
+
delete ctx.window;
|
|
41
|
+
delete ctx.WebSocket;
|
|
42
|
+
delete ctx.pluginHostAPI;
|
|
43
|
+
|
|
44
|
+
const replFilter = new Set<string>(['require', 'localStorage'])
|
|
45
|
+
const replVariables = Object.keys(ctx).filter(key => !replFilter.has(key));
|
|
46
|
+
|
|
47
|
+
const welcome = `JavaScript REPL variables:\n${replVariables.map(key => ' ' + key).join('\n')}\n\n`;
|
|
48
|
+
socket.write(welcome);
|
|
49
|
+
|
|
50
|
+
const r = repl.start({
|
|
51
|
+
terminal: true,
|
|
52
|
+
input: socket,
|
|
53
|
+
output: socket,
|
|
54
|
+
// writer(this: REPLServer, obj: any) {
|
|
55
|
+
// const ret = util.inspect(obj, {
|
|
56
|
+
// colors: true,
|
|
57
|
+
// });
|
|
58
|
+
// return ret;//.replaceAll('\n', '\r\n');
|
|
59
|
+
// },
|
|
60
|
+
preview: false,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
Object.assign(r.context, ctx);
|
|
64
|
+
|
|
65
|
+
const cleanup = () => {
|
|
66
|
+
r.close();
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
socket.on('close', cleanup);
|
|
70
|
+
socket.on('error', cleanup);
|
|
71
|
+
socket.on('end', cleanup);
|
|
72
|
+
});
|
|
73
|
+
return listenZero(server);
|
|
74
|
+
}
|
package/src/runtime.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Level } from './level';
|
|
2
2
|
import { PluginHost } from './plugin/plugin-host';
|
|
3
|
-
import cluster from 'cluster';
|
|
4
3
|
import { ScryptedNativeId, Device, EngineIOHandler, HttpRequest, HttpRequestHandler, OauthClient, PushHandler, ScryptedDevice, ScryptedInterface, ScryptedInterfaceProperty } from '@scrypted/sdk/types';
|
|
5
4
|
import { PluginDeviceProxyHandler } from './plugin/plugin-device';
|
|
6
5
|
import { Plugin, PluginDevice, ScryptedAlert } from './db-types';
|
|
@@ -416,6 +415,7 @@ export class ScryptedRuntime {
|
|
|
416
415
|
}
|
|
417
416
|
}
|
|
418
417
|
|
|
418
|
+
// should this be async?
|
|
419
419
|
invalidatePluginDevice(id: string) {
|
|
420
420
|
const proxyPair = this.devices[id];
|
|
421
421
|
if (!proxyPair)
|
|
@@ -487,29 +487,6 @@ export class ScryptedRuntime {
|
|
|
487
487
|
this.invalidatePluginDevice(pluginDevice._id);
|
|
488
488
|
}
|
|
489
489
|
|
|
490
|
-
const execArgv = [
|
|
491
|
-
'--expose-gc',
|
|
492
|
-
];
|
|
493
|
-
if (process.argv[0].endsWith('ts-node'))
|
|
494
|
-
execArgv.push('-r', 'ts-node/register');
|
|
495
|
-
|
|
496
|
-
if (pluginDebug) {
|
|
497
|
-
console.log('plugin inspect port', pluginDebug.inspectPort)
|
|
498
|
-
execArgv.push('--inspect');
|
|
499
|
-
|
|
500
|
-
cluster.setupMaster({
|
|
501
|
-
silent: true,
|
|
502
|
-
inspectPort: pluginDebug.inspectPort,
|
|
503
|
-
execArgv,
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
else {
|
|
507
|
-
cluster.setupMaster({
|
|
508
|
-
silent: true,
|
|
509
|
-
execArgv,
|
|
510
|
-
});
|
|
511
|
-
}
|
|
512
|
-
|
|
513
490
|
const pluginHost = new PluginHost(this, plugin, pluginDebug);
|
|
514
491
|
pluginHost.worker.once('exit', () => {
|
|
515
492
|
if (pluginHost.killed)
|
|
@@ -589,6 +566,7 @@ export class ScryptedRuntime {
|
|
|
589
566
|
}
|
|
590
567
|
device.state = undefined;
|
|
591
568
|
|
|
569
|
+
this.invalidatePluginDevice(device._id);
|
|
592
570
|
delete this.pluginDevices[device._id];
|
|
593
571
|
await this.datastore.remove(device);
|
|
594
572
|
if (providerId == null || providerId === device._id) {
|