@scrypted/server 0.0.83 → 0.0.87
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-host.js +67 -168
- 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/scrypted-main.js +15 -30
- 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 +1 -1
- package/python/plugin-remote.py +70 -30
- package/python/rpc.py +1 -1
- package/src/plugin/plugin-console.ts +115 -0
- package/src/plugin/plugin-host.ts +67 -194
- 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/scrypted-main.ts +15 -32
- package/src/services/plugin.ts +8 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { ScryptedNativeId } from '@scrypted/sdk/types'
|
|
2
|
+
import { EventEmitter } from 'ws';
|
|
3
|
+
import { listenZero } from './listen-zero';
|
|
4
|
+
import { Server } from 'net';
|
|
5
|
+
import { once } from 'events';
|
|
6
|
+
import net from 'net'
|
|
7
|
+
import { Readable } from 'stream';
|
|
8
|
+
|
|
9
|
+
export interface ConsoleServer {
|
|
10
|
+
readPort: number,
|
|
11
|
+
writePort: number,
|
|
12
|
+
readServer: net.Server,
|
|
13
|
+
writeServer: net.Server,
|
|
14
|
+
sockets: Set<net.Socket>;
|
|
15
|
+
}
|
|
16
|
+
export async function createConsoleServer(stdout: Readable, stderr: Readable) {
|
|
17
|
+
const outputs = new Map<string, Buffer[]>();
|
|
18
|
+
const appendOutput = (data: Buffer, nativeId: ScryptedNativeId) => {
|
|
19
|
+
if (!nativeId)
|
|
20
|
+
nativeId = undefined;
|
|
21
|
+
let buffers = outputs.get(nativeId);
|
|
22
|
+
if (!buffers) {
|
|
23
|
+
buffers = [];
|
|
24
|
+
outputs.set(nativeId, buffers);
|
|
25
|
+
}
|
|
26
|
+
buffers.push(data);
|
|
27
|
+
// when we're over 4000 lines or whatever these buffer are,
|
|
28
|
+
// truncate down to 2000.
|
|
29
|
+
if (buffers.length > 4000)
|
|
30
|
+
outputs.set(nativeId, buffers.slice(buffers.length - 2000))
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const sockets = new Set<net.Socket>();
|
|
34
|
+
|
|
35
|
+
const events = new EventEmitter();
|
|
36
|
+
events.on('stdout', appendOutput);
|
|
37
|
+
events.on('stderr', appendOutput);
|
|
38
|
+
|
|
39
|
+
stdout.on('data', data => events.emit('stdout', data));
|
|
40
|
+
stderr.on('data', data => events.emit('stderr', data));
|
|
41
|
+
|
|
42
|
+
const readServer = new Server(async (socket) => {
|
|
43
|
+
sockets.add(socket);
|
|
44
|
+
|
|
45
|
+
let [filter] = await once(socket, 'data');
|
|
46
|
+
filter = filter.toString().trim();
|
|
47
|
+
if (filter === 'undefined')
|
|
48
|
+
filter = undefined;
|
|
49
|
+
|
|
50
|
+
const buffers = outputs.get(filter);
|
|
51
|
+
if (buffers) {
|
|
52
|
+
const concat = Buffer.concat(buffers);
|
|
53
|
+
outputs.set(filter, [concat]);
|
|
54
|
+
socket.write(concat);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const cb = (data: Buffer, nativeId: ScryptedNativeId) => {
|
|
58
|
+
if (nativeId !== filter)
|
|
59
|
+
return;
|
|
60
|
+
socket.write(data);
|
|
61
|
+
};
|
|
62
|
+
events.on('stdout', cb)
|
|
63
|
+
events.on('stderr', cb)
|
|
64
|
+
|
|
65
|
+
const cleanup = () => {
|
|
66
|
+
events.removeListener('stdout', cb);
|
|
67
|
+
events.removeListener('stderr', cb);
|
|
68
|
+
socket.destroy();
|
|
69
|
+
socket.removeAllListeners();
|
|
70
|
+
sockets.delete(socket);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
socket.on('close', cleanup);
|
|
74
|
+
socket.on('error', cleanup);
|
|
75
|
+
socket.on('end', cleanup);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const writeServer = new Server(async (socket) => {
|
|
79
|
+
sockets.add(socket);
|
|
80
|
+
const [data] = await once(socket, 'data');
|
|
81
|
+
let filter: string = data.toString();
|
|
82
|
+
const newline = filter.indexOf('\n');
|
|
83
|
+
if (newline !== -1) {
|
|
84
|
+
socket.unshift(Buffer.from(filter.substring(newline + 1)));
|
|
85
|
+
}
|
|
86
|
+
filter = filter.substring(0, newline);
|
|
87
|
+
|
|
88
|
+
if (filter === 'undefined')
|
|
89
|
+
filter = undefined;
|
|
90
|
+
|
|
91
|
+
const cb = (data: Buffer) => events.emit('stdout', data, filter);
|
|
92
|
+
|
|
93
|
+
socket.on('data', cb);
|
|
94
|
+
|
|
95
|
+
const cleanup = () => {
|
|
96
|
+
socket.destroy();
|
|
97
|
+
socket.removeAllListeners();
|
|
98
|
+
sockets.delete(socket);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
socket.once('close', cleanup);
|
|
102
|
+
socket.once('error', cleanup);
|
|
103
|
+
socket.once('end', cleanup);
|
|
104
|
+
});
|
|
105
|
+
const readPort = await listenZero(readServer);
|
|
106
|
+
const writePort = await listenZero(writeServer);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
readPort,
|
|
110
|
+
writePort,
|
|
111
|
+
readServer,
|
|
112
|
+
writeServer,
|
|
113
|
+
sockets,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -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
|
|
|
@@ -228,9 +235,9 @@ export class PluginHost {
|
|
|
228
235
|
path.join(__dirname, '../../python', 'plugin-remote.py'),
|
|
229
236
|
)
|
|
230
237
|
|
|
231
|
-
this.worker = child_process.spawn('
|
|
238
|
+
this.worker = child_process.spawn('python3', args, {
|
|
232
239
|
// stdin, stdout, stderr, peer in, peer out
|
|
233
|
-
stdio: ['pipe', '
|
|
240
|
+
stdio: ['pipe', 'pipe', 'pipe', 'pipe', 'pipe'],
|
|
234
241
|
env: Object.assign({}, process.env, env),
|
|
235
242
|
});
|
|
236
243
|
|
|
@@ -264,7 +271,7 @@ export class PluginHost {
|
|
|
264
271
|
}
|
|
265
272
|
|
|
266
273
|
this.worker = child_process.fork(require.main.filename, ['child'], {
|
|
267
|
-
stdio: ['pipe', '
|
|
274
|
+
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
|
|
268
275
|
env: Object.assign({}, process.env, env),
|
|
269
276
|
serialization: 'advanced',
|
|
270
277
|
execArgv,
|
|
@@ -286,8 +293,9 @@ export class PluginHost {
|
|
|
286
293
|
this.worker.on('message', message => this.peer.handleMessage(message as any));
|
|
287
294
|
}
|
|
288
295
|
|
|
289
|
-
|
|
290
|
-
|
|
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);
|
|
291
299
|
|
|
292
300
|
this.worker.on('disconnect', () => {
|
|
293
301
|
connected = false;
|
|
@@ -310,157 +318,6 @@ export class PluginHost {
|
|
|
310
318
|
}
|
|
311
319
|
}
|
|
312
320
|
|
|
313
|
-
async function createConsoleServer(events: EventEmitter): Promise<number[]> {
|
|
314
|
-
const outputs = new Map<string, Buffer[]>();
|
|
315
|
-
const appendOutput = (data: Buffer, nativeId: ScryptedNativeId) => {
|
|
316
|
-
if (!nativeId)
|
|
317
|
-
nativeId = undefined;
|
|
318
|
-
let buffers = outputs.get(nativeId);
|
|
319
|
-
if (!buffers) {
|
|
320
|
-
buffers = [];
|
|
321
|
-
outputs.set(nativeId, buffers);
|
|
322
|
-
}
|
|
323
|
-
buffers.push(data);
|
|
324
|
-
// when we're over 4000 lines or whatever these buffer are,
|
|
325
|
-
// truncate down to 2000.
|
|
326
|
-
if (buffers.length > 4000)
|
|
327
|
-
outputs.set(nativeId, buffers.slice(buffers.length - 2000))
|
|
328
|
-
};
|
|
329
|
-
events.on('stdout', appendOutput);
|
|
330
|
-
events.on('stderr', appendOutput);
|
|
331
|
-
|
|
332
|
-
const server = new Server(async (socket) => {
|
|
333
|
-
let [filter] = await once(socket, 'data');
|
|
334
|
-
filter = filter.toString().trim();
|
|
335
|
-
if (filter === 'undefined')
|
|
336
|
-
filter = undefined;
|
|
337
|
-
|
|
338
|
-
const buffers = outputs.get(filter);
|
|
339
|
-
if (buffers) {
|
|
340
|
-
const concat = Buffer.concat(buffers);
|
|
341
|
-
outputs.set(filter, [concat]);
|
|
342
|
-
socket.write(concat);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const cb = (data: Buffer, nativeId: ScryptedNativeId) => {
|
|
346
|
-
if (nativeId !== filter)
|
|
347
|
-
return;
|
|
348
|
-
socket.write(data);
|
|
349
|
-
};
|
|
350
|
-
events.on('stdout', cb)
|
|
351
|
-
events.on('stderr', cb)
|
|
352
|
-
|
|
353
|
-
const cleanup = () => {
|
|
354
|
-
events.removeListener('stdout', cb);
|
|
355
|
-
events.removeListener('stderr', cb);
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
socket.on('close', cleanup);
|
|
359
|
-
socket.on('error', cleanup);
|
|
360
|
-
socket.on('end', cleanup);
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const writeServer = new Server(async (socket) => {
|
|
365
|
-
const [data] = await once(socket, 'data');
|
|
366
|
-
let filter: string = data.toString();
|
|
367
|
-
const newline = filter.indexOf('\n');
|
|
368
|
-
if (newline !== -1) {
|
|
369
|
-
socket.unshift(Buffer.from(filter.substring(newline + 1)));
|
|
370
|
-
}
|
|
371
|
-
filter = filter.substring(0, newline);
|
|
372
|
-
|
|
373
|
-
if (filter === 'undefined')
|
|
374
|
-
filter = undefined;
|
|
375
|
-
|
|
376
|
-
const cb = (data: Buffer) => events.emit('stdout', data, filter);
|
|
377
|
-
|
|
378
|
-
socket.on('data', cb);
|
|
379
|
-
|
|
380
|
-
const cleanup = () => {
|
|
381
|
-
events.removeListener('data', cb);
|
|
382
|
-
};
|
|
383
|
-
|
|
384
|
-
socket.on('close', cleanup);
|
|
385
|
-
socket.on('error', cleanup);
|
|
386
|
-
socket.on('end', cleanup);
|
|
387
|
-
});
|
|
388
|
-
const consoleReader = await listenZero(server);
|
|
389
|
-
const consoleWriter = await listenZero(writeServer);
|
|
390
|
-
|
|
391
|
-
return [consoleReader, consoleWriter];
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
async function createREPLServer(events: EventEmitter): Promise<number> {
|
|
395
|
-
const [[scrypted], [params], [plugin]] = await Promise.all([once(events, 'scrypted'), once(events, 'params'), once(events, 'plugin')]);
|
|
396
|
-
const { deviceManager, systemManager } = scrypted;
|
|
397
|
-
const server = new Server(async (socket) => {
|
|
398
|
-
let [filter] = await once(socket, 'data');
|
|
399
|
-
filter = filter.toString().trim();
|
|
400
|
-
if (filter === 'undefined')
|
|
401
|
-
filter = undefined;
|
|
402
|
-
|
|
403
|
-
const chain: string[] = [];
|
|
404
|
-
const nativeIds: Map<string, any> = deviceManager.nativeIds;
|
|
405
|
-
const reversed = new Map<string, string>();
|
|
406
|
-
for (const nativeId of nativeIds.keys()) {
|
|
407
|
-
reversed.set(nativeIds.get(nativeId).id, nativeId);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
while (filter) {
|
|
411
|
-
const { id } = nativeIds.get(filter);
|
|
412
|
-
const d = await systemManager.getDeviceById(id);
|
|
413
|
-
chain.push(filter);
|
|
414
|
-
filter = reversed.get(d.providerId);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
chain.reverse();
|
|
418
|
-
let device = plugin;
|
|
419
|
-
for (const c of chain) {
|
|
420
|
-
device = await device.getDevice(c);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const ctx = Object.assign(params, {
|
|
425
|
-
device
|
|
426
|
-
});
|
|
427
|
-
delete ctx.console;
|
|
428
|
-
delete ctx.window;
|
|
429
|
-
delete ctx.WebSocket;
|
|
430
|
-
delete ctx.pluginHostAPI;
|
|
431
|
-
|
|
432
|
-
const replFilter = new Set<string>(['require', 'localStorage'])
|
|
433
|
-
const replVariables = Object.keys(ctx).filter(key => !replFilter.has(key));
|
|
434
|
-
|
|
435
|
-
const welcome = `JavaScript REPL variables:\n${replVariables.map(key => ' ' + key).join('\n')}\n\n`;
|
|
436
|
-
socket.write(welcome);
|
|
437
|
-
|
|
438
|
-
const r = repl.start({
|
|
439
|
-
terminal: true,
|
|
440
|
-
input: socket,
|
|
441
|
-
output: socket,
|
|
442
|
-
// writer(this: REPLServer, obj: any) {
|
|
443
|
-
// const ret = util.inspect(obj, {
|
|
444
|
-
// colors: true,
|
|
445
|
-
// });
|
|
446
|
-
// return ret;//.replaceAll('\n', '\r\n');
|
|
447
|
-
// },
|
|
448
|
-
preview: false,
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
Object.assign(r.context, ctx);
|
|
452
|
-
|
|
453
|
-
const cleanup = () => {
|
|
454
|
-
r.close();
|
|
455
|
-
};
|
|
456
|
-
|
|
457
|
-
socket.on('close', cleanup);
|
|
458
|
-
socket.on('error', cleanup);
|
|
459
|
-
socket.on('end', cleanup);
|
|
460
|
-
});
|
|
461
|
-
return listenZero(server);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
321
|
export function startPluginRemote() {
|
|
465
322
|
const peer = new RpcPeer('unknown', 'host', (message, reject) => process.send(message, undefined, {
|
|
466
323
|
swallowErrors: !reject,
|
|
@@ -471,10 +328,10 @@ export function startPluginRemote() {
|
|
|
471
328
|
peer.transportSafeArgumentTypes.add(Buffer.name);
|
|
472
329
|
process.on('message', message => peer.handleMessage(message as RpcMessage));
|
|
473
330
|
|
|
474
|
-
const events = new EventEmitter();
|
|
475
|
-
|
|
476
331
|
let systemManager: SystemManager;
|
|
477
332
|
let deviceManager: DeviceManager;
|
|
333
|
+
let api: PluginAPI;
|
|
334
|
+
let pluginId: string;
|
|
478
335
|
|
|
479
336
|
function idForNativeId(nativeId: ScryptedNativeId) {
|
|
480
337
|
if (!deviceManager)
|
|
@@ -528,16 +385,31 @@ export function startPluginRemote() {
|
|
|
528
385
|
|
|
529
386
|
const getDeviceConsole = (nativeId?: ScryptedNativeId) => {
|
|
530
387
|
return getConsole(async (stdout, stderr) => {
|
|
531
|
-
|
|
532
|
-
|
|
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();
|
|
533
407
|
}, undefined, undefined);
|
|
534
408
|
}
|
|
535
409
|
|
|
536
410
|
const getMixinConsole = (mixinId: string, nativeId?: ScryptedNativeId) => {
|
|
537
411
|
return getConsole(async (stdout, stderr) => {
|
|
538
412
|
if (!mixinId || !systemManager.getDeviceById(mixinId).mixins.includes(idForNativeId(nativeId))) {
|
|
539
|
-
stdout.on('data', data => events.emit('stdout', data, nativeId));
|
|
540
|
-
stderr.on('data', data => events.emit('stderr', data, nativeId));
|
|
541
413
|
return;
|
|
542
414
|
}
|
|
543
415
|
const plugins = await systemManager.getComponent('plugins');
|
|
@@ -583,28 +455,40 @@ export function startPluginRemote() {
|
|
|
583
455
|
global?.gc();
|
|
584
456
|
}, 10000);
|
|
585
457
|
|
|
586
|
-
|
|
587
|
-
const replPort = createREPLServer(events);
|
|
458
|
+
let replPort: Promise<number>;
|
|
588
459
|
|
|
589
|
-
|
|
460
|
+
let _pluginConsole: Console;
|
|
461
|
+
const getPluginConsole = () => {
|
|
462
|
+
if (_pluginConsole)
|
|
463
|
+
return _pluginConsole;
|
|
464
|
+
_pluginConsole = getDeviceConsole(undefined);
|
|
465
|
+
}
|
|
590
466
|
|
|
591
467
|
attachPluginRemote(peer, {
|
|
592
|
-
createMediaManager: async (
|
|
593
|
-
|
|
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,
|
|
594
481
|
getDeviceConsole,
|
|
595
482
|
getMixinConsole,
|
|
596
483
|
async getServicePort(name) {
|
|
597
|
-
if (name === 'repl')
|
|
484
|
+
if (name === 'repl') {
|
|
485
|
+
if (!replPort)
|
|
486
|
+
throw new Error('REPL unavailable: Plugin not loaded.')
|
|
598
487
|
return replPort;
|
|
599
|
-
|
|
600
|
-
return (await consolePorts)[0];
|
|
601
|
-
if (name === 'console-writer')
|
|
602
|
-
return (await consolePorts)[1];
|
|
488
|
+
}
|
|
603
489
|
throw new Error(`unknown service ${name}`);
|
|
604
490
|
},
|
|
605
|
-
async
|
|
606
|
-
const pluginId = packageJson.name;
|
|
607
|
-
peer.selfName = pluginId;
|
|
491
|
+
async onLoadZip(zip: AdmZip, packageJson: any) {
|
|
608
492
|
installSourceMapSupport({
|
|
609
493
|
environment: 'node',
|
|
610
494
|
retrieveSourceMap(source) {
|
|
@@ -621,29 +505,18 @@ export function startPluginRemote() {
|
|
|
621
505
|
return null;
|
|
622
506
|
}
|
|
623
507
|
});
|
|
624
|
-
|
|
625
|
-
const writer = cp[1];
|
|
626
|
-
const socket = net.connect(writer);
|
|
627
|
-
await once(socket, 'connect');
|
|
628
|
-
try {
|
|
629
|
-
await installOptionalDependencies(pluginConsole, socket, packageJson);
|
|
630
|
-
}
|
|
631
|
-
finally {
|
|
632
|
-
socket.destroy();
|
|
633
|
-
}
|
|
508
|
+
await installOptionalDependencies(getPluginConsole(), packageJson);
|
|
634
509
|
}
|
|
635
510
|
}).then(scrypted => {
|
|
636
511
|
systemManager = scrypted.systemManager;
|
|
637
512
|
deviceManager = scrypted.deviceManager;
|
|
638
513
|
|
|
639
|
-
events.emit('scrypted', scrypted);
|
|
640
|
-
|
|
641
514
|
process.on('uncaughtException', e => {
|
|
642
|
-
|
|
515
|
+
getPluginConsole().error('uncaughtException', e);
|
|
643
516
|
scrypted.log.e('uncaughtException ' + e?.toString());
|
|
644
517
|
});
|
|
645
518
|
process.on('unhandledRejection', e => {
|
|
646
|
-
|
|
519
|
+
getPluginConsole().error('unhandledRejection', e);
|
|
647
520
|
scrypted.log.e('unhandledRejection ' + e?.toString());
|
|
648
521
|
});
|
|
649
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
|
+
}
|