@scrypted/server 0.0.71 → 0.0.78
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/settings.json +2 -1
- package/dist/cert.js +75 -0
- package/dist/cert.js.map +1 -0
- package/dist/plugin/media.js +50 -14
- package/dist/plugin/media.js.map +1 -1
- package/dist/plugin/plugin-device.js +11 -1
- package/dist/plugin/plugin-device.js.map +1 -1
- package/dist/plugin/plugin-host-api.js +7 -3
- package/dist/plugin/plugin-host-api.js.map +1 -1
- package/dist/plugin/plugin-host.js +66 -45
- package/dist/plugin/plugin-host.js.map +1 -1
- package/dist/plugin/plugin-npm-dependencies.js +53 -0
- package/dist/plugin/plugin-npm-dependencies.js.map +1 -0
- package/dist/plugin/plugin-remote.js +5 -5
- package/dist/plugin/plugin-remote.js.map +1 -1
- package/dist/plugin/plugin-volume.js +20 -0
- package/dist/plugin/plugin-volume.js.map +1 -0
- package/dist/plugin/system.js +9 -3
- package/dist/plugin/system.js.map +1 -1
- package/dist/rpc.js +32 -12
- package/dist/rpc.js.map +1 -1
- package/dist/runtime.js +20 -27
- package/dist/runtime.js.map +1 -1
- package/dist/scrypted-main.js +4 -19
- package/dist/scrypted-main.js.map +1 -1
- package/package.json +4 -3
- package/python/media.py +40 -0
- package/python/plugin-remote.py +67 -30
- package/python/rpc.py +24 -81
- package/src/cert.ts +74 -0
- package/src/plugin/media.ts +53 -14
- package/src/plugin/plugin-api.ts +0 -1
- package/src/plugin/plugin-device.ts +13 -2
- package/src/plugin/plugin-host-api.ts +15 -3
- package/src/plugin/plugin-host.ts +69 -49
- package/src/plugin/plugin-npm-dependencies.ts +55 -0
- package/src/plugin/plugin-remote.ts +8 -7
- package/src/plugin/plugin-volume.ts +13 -0
- package/src/plugin/system.ts +11 -3
- package/src/rpc.ts +35 -12
- package/src/runtime.ts +24 -30
- package/src/scrypted-main.ts +5 -24
|
@@ -7,7 +7,7 @@ import io from 'engine.io';
|
|
|
7
7
|
import { attachPluginRemote, setupPluginRemote } from './plugin-remote';
|
|
8
8
|
import { PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
|
|
9
9
|
import { Logger } from '../logger';
|
|
10
|
-
import { MediaManagerImpl } from './media';
|
|
10
|
+
import { MediaManagerHostImpl, MediaManagerImpl } from './media';
|
|
11
11
|
import { getState } from '../state';
|
|
12
12
|
import WebSocket, { EventEmitter } from 'ws';
|
|
13
13
|
import { listenZero } from './listen-zero';
|
|
@@ -18,7 +18,6 @@ import { PassThrough } from 'stream';
|
|
|
18
18
|
import { Console } from 'console'
|
|
19
19
|
import { sleep } from '../sleep';
|
|
20
20
|
import { PluginHostAPI } from './plugin-host-api';
|
|
21
|
-
import mkdirp from 'mkdirp';
|
|
22
21
|
import path from 'path';
|
|
23
22
|
import { install as installSourceMapSupport } from 'source-map-support';
|
|
24
23
|
import net from 'net'
|
|
@@ -26,17 +25,8 @@ import child_process from 'child_process';
|
|
|
26
25
|
import { PluginDebug } from './plugin-debug';
|
|
27
26
|
import readline from 'readline';
|
|
28
27
|
import { Readable, Writable } from 'stream';
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const volume = path.join(process.cwd(), 'volume');
|
|
32
|
-
const pluginVolume = path.join(volume, 'plugins', pluginId);
|
|
33
|
-
try {
|
|
34
|
-
mkdirp.sync(pluginVolume);
|
|
35
|
-
}
|
|
36
|
-
catch (e) {
|
|
37
|
-
}
|
|
38
|
-
return pluginVolume;
|
|
39
|
-
}
|
|
28
|
+
import { ensurePluginVolume } from './plugin-volume';
|
|
29
|
+
import { installOptionalDependencies } from './plugin-npm-dependencies';
|
|
40
30
|
|
|
41
31
|
export class PluginHost {
|
|
42
32
|
worker: child_process.ChildProcess;
|
|
@@ -58,8 +48,10 @@ export class PluginHost {
|
|
|
58
48
|
cpuUsage: NodeJS.CpuUsage,
|
|
59
49
|
memoryUsage: NodeJS.MemoryUsage,
|
|
60
50
|
};
|
|
51
|
+
killed = false;
|
|
61
52
|
|
|
62
53
|
kill() {
|
|
54
|
+
this.killed = true;
|
|
63
55
|
this.listener.removeListener();
|
|
64
56
|
this.api.removeListeners();
|
|
65
57
|
this.worker.kill();
|
|
@@ -134,7 +126,12 @@ export class PluginHost {
|
|
|
134
126
|
|
|
135
127
|
const self = this;
|
|
136
128
|
|
|
137
|
-
|
|
129
|
+
const { runtime } = this.packageJson.scrypted;
|
|
130
|
+
const mediaManager = runtime === 'python'
|
|
131
|
+
? new MediaManagerHostImpl(scrypted.stateManager.getSystemState(), id => scrypted.getDevice(id), console)
|
|
132
|
+
: undefined;
|
|
133
|
+
|
|
134
|
+
this.api = new PluginHostAPI(scrypted, plugin, this, mediaManager);
|
|
138
135
|
|
|
139
136
|
const zipBuffer = Buffer.from(plugin.zip, 'base64');
|
|
140
137
|
this.zip = new AdmZip(zipBuffer);
|
|
@@ -165,7 +162,6 @@ export class PluginHost {
|
|
|
165
162
|
}
|
|
166
163
|
}
|
|
167
164
|
|
|
168
|
-
const { runtime } = this.packageJson.scrypted;
|
|
169
165
|
const fail = 'Plugin failed to load. Console for more information.';
|
|
170
166
|
try {
|
|
171
167
|
const loadZipOptions: PluginRemoteLoadZipOptions = {
|
|
@@ -192,8 +188,6 @@ export class PluginHost {
|
|
|
192
188
|
}
|
|
193
189
|
})();
|
|
194
190
|
|
|
195
|
-
init.catch(e => console.error('plugin failed to load', e));
|
|
196
|
-
|
|
197
191
|
this.module = init.then(({ module }) => module);
|
|
198
192
|
this.remote = new LazyRemote(remotePromise, init.then(({ remote }) => remote));
|
|
199
193
|
|
|
@@ -206,13 +200,20 @@ export class PluginHost {
|
|
|
206
200
|
this.remote.notify(id, eventDetails.eventTime, eventDetails.eventInterface, eventDetails.property, eventData, eventDetails.changed);
|
|
207
201
|
}
|
|
208
202
|
});
|
|
203
|
+
|
|
204
|
+
init.catch(e => {
|
|
205
|
+
console.error('plugin failed to load', e);
|
|
206
|
+
this.listener.removeListener();
|
|
207
|
+
});
|
|
209
208
|
}
|
|
210
209
|
|
|
211
210
|
startPluginClusterHost(logger: Logger, env?: any, runtime?: string) {
|
|
212
211
|
let connected = true;
|
|
213
212
|
|
|
214
213
|
if (runtime === 'python') {
|
|
215
|
-
const args: string[] = [
|
|
214
|
+
const args: string[] = [
|
|
215
|
+
'-u',
|
|
216
|
+
];
|
|
216
217
|
if (this.pluginDebug) {
|
|
217
218
|
args.push(
|
|
218
219
|
'-m',
|
|
@@ -235,6 +236,9 @@ export class PluginHost {
|
|
|
235
236
|
const peerin = this.worker.stdio[3] as Writable;
|
|
236
237
|
const peerout = this.worker.stdio[4] as Readable;
|
|
237
238
|
|
|
239
|
+
peerin.on('error', e => connected = false);
|
|
240
|
+
peerout.on('error', e => connected = false);
|
|
241
|
+
|
|
238
242
|
this.peer = new RpcPeer('host', this.pluginId, (message, reject) => {
|
|
239
243
|
if (connected) {
|
|
240
244
|
peerin.write(JSON.stringify(message) + '\n', e => e && reject?.(e));
|
|
@@ -281,10 +285,8 @@ export class PluginHost {
|
|
|
281
285
|
this.worker.on('message', message => this.peer.handleMessage(message as any));
|
|
282
286
|
}
|
|
283
287
|
|
|
284
|
-
this.worker.stdout.on('data', data =>
|
|
285
|
-
|
|
286
|
-
});
|
|
287
|
-
this.worker.stderr.on('data', data => process.stderr.write(data));
|
|
288
|
+
this.worker.stdout.on('data', data => console.log(data.toString()));
|
|
289
|
+
this.worker.stderr.on('data', data => console.error(data.toString()));
|
|
288
290
|
|
|
289
291
|
this.worker.on('disconnect', () => {
|
|
290
292
|
connected = false;
|
|
@@ -470,28 +472,6 @@ export function startPluginClusterWorker() {
|
|
|
470
472
|
|
|
471
473
|
const events = new EventEmitter();
|
|
472
474
|
|
|
473
|
-
events.once('zip', (zip: AdmZip, pluginId: string) => {
|
|
474
|
-
peer.selfName = pluginId;
|
|
475
|
-
|
|
476
|
-
installSourceMapSupport({
|
|
477
|
-
environment: 'node',
|
|
478
|
-
retrieveSourceMap(source) {
|
|
479
|
-
if (source === '/plugin/main.nodejs.js' || source === `/${pluginId}/main.nodejs.js`) {
|
|
480
|
-
const entry = zip.getEntry('main.nodejs.js.map')
|
|
481
|
-
const map = entry?.getData().toString();
|
|
482
|
-
if (!map)
|
|
483
|
-
return null;
|
|
484
|
-
|
|
485
|
-
return {
|
|
486
|
-
url: '/plugin/main.nodejs.js',
|
|
487
|
-
map,
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
return null;
|
|
491
|
-
}
|
|
492
|
-
})
|
|
493
|
-
});
|
|
494
|
-
|
|
495
475
|
let systemManager: SystemManager;
|
|
496
476
|
let deviceManager: DeviceManager;
|
|
497
477
|
|
|
@@ -620,6 +600,36 @@ export function startPluginClusterWorker() {
|
|
|
620
600
|
if (name === 'console-writer')
|
|
621
601
|
return (await consolePorts)[1];
|
|
622
602
|
throw new Error(`unknown service ${name}`);
|
|
603
|
+
},
|
|
604
|
+
async beforeLoadZip(zip: AdmZip, packageJson: any) {
|
|
605
|
+
const pluginId = packageJson.name;
|
|
606
|
+
peer.selfName = pluginId;
|
|
607
|
+
installSourceMapSupport({
|
|
608
|
+
environment: 'node',
|
|
609
|
+
retrieveSourceMap(source) {
|
|
610
|
+
if (source === '/plugin/main.nodejs.js' || source === `/${pluginId}/main.nodejs.js`) {
|
|
611
|
+
const entry = zip.getEntry('main.nodejs.js.map')
|
|
612
|
+
const map = entry?.getData().toString();
|
|
613
|
+
if (!map)
|
|
614
|
+
return null;
|
|
615
|
+
return {
|
|
616
|
+
url: '/plugin/main.nodejs.js',
|
|
617
|
+
map,
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
const cp = await consolePorts;
|
|
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
|
+
}
|
|
623
633
|
}
|
|
624
634
|
}).then(scrypted => {
|
|
625
635
|
systemManager = scrypted.systemManager;
|
|
@@ -648,10 +658,20 @@ class LazyRemote implements PluginRemote {
|
|
|
648
658
|
})();
|
|
649
659
|
}
|
|
650
660
|
|
|
661
|
+
async ensureRemote() {
|
|
662
|
+
try {
|
|
663
|
+
if (!this.remote)
|
|
664
|
+
await this.remoteReadyPromise;
|
|
665
|
+
}
|
|
666
|
+
catch (e) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
return true;
|
|
670
|
+
}
|
|
671
|
+
|
|
651
672
|
async loadZip(packageJson: any, zipData: Buffer, options?: PluginRemoteLoadZipOptions): Promise<any> {
|
|
652
673
|
if (!this.remote)
|
|
653
674
|
await this.remoteReadyPromise;
|
|
654
|
-
|
|
655
675
|
return this.remote.loadZip(packageJson, zipData, options);
|
|
656
676
|
}
|
|
657
677
|
async setSystemState(state: { [id: string]: { [property: string]: SystemDeviceState; }; }): Promise<void> {
|
|
@@ -665,13 +685,13 @@ class LazyRemote implements PluginRemote {
|
|
|
665
685
|
return this.remote.setNativeId(nativeId, id, storage);
|
|
666
686
|
}
|
|
667
687
|
async updateDeviceState(id: string, state: { [property: string]: SystemDeviceState; }): Promise<void> {
|
|
668
|
-
if (!this.
|
|
669
|
-
|
|
688
|
+
if (!await this.ensureRemote())
|
|
689
|
+
return;
|
|
670
690
|
return this.remote.updateDeviceState(id, state);
|
|
671
691
|
}
|
|
672
692
|
async notify(id: string, eventTime: number, eventInterface: string, property: string, propertyState: SystemDeviceState, changed?: boolean): Promise<void> {
|
|
673
|
-
if (!this.
|
|
674
|
-
|
|
693
|
+
if (!await this.ensureRemote())
|
|
694
|
+
return;
|
|
675
695
|
return this.remote.notify(id, eventTime, eventInterface, property, propertyState, changed);
|
|
676
696
|
}
|
|
677
697
|
async ioEvent(id: string, event: string, message?: any): Promise<void> {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ensurePluginVolume } from "./plugin-volume";
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import child_process from 'child_process';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { once } from 'events';
|
|
6
|
+
import { Socket } from "net";
|
|
7
|
+
|
|
8
|
+
export async function installOptionalDependencies(console: Console, socket: Socket, packageJson: any) {
|
|
9
|
+
const pluginVolume = ensurePluginVolume(packageJson.name);
|
|
10
|
+
const optPj = path.join(pluginVolume, 'package.json');
|
|
11
|
+
|
|
12
|
+
let currentPackageJson: any;
|
|
13
|
+
try {
|
|
14
|
+
currentPackageJson = JSON.parse(fs.readFileSync(optPj).toString());
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { optionalDependencies } = packageJson;
|
|
20
|
+
if (!optionalDependencies)
|
|
21
|
+
return;
|
|
22
|
+
if (!Object.keys(optionalDependencies).length)
|
|
23
|
+
return;
|
|
24
|
+
const currentOptionalDependencies = currentPackageJson?.dependencies || {};
|
|
25
|
+
|
|
26
|
+
if (JSON.stringify(optionalDependencies) === JSON.stringify(currentOptionalDependencies)) {
|
|
27
|
+
console.log('native dependencies (up to date).', ...Object.keys(optionalDependencies));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log('native dependencies (outdated)', ...Object.keys(optionalDependencies));
|
|
32
|
+
|
|
33
|
+
const reduced = Object.assign({}, packageJson);
|
|
34
|
+
reduced.dependencies = reduced.optionalDependencies;
|
|
35
|
+
delete reduced.optionalDependencies;
|
|
36
|
+
delete reduced.devDependencies;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
fs.writeFileSync(optPj, JSON.stringify(reduced));
|
|
40
|
+
|
|
41
|
+
const cp = child_process.spawn('npm', ['--prefix', pluginVolume, 'install'], {
|
|
42
|
+
cwd: pluginVolume,
|
|
43
|
+
stdio: ['inherit', socket, socket],
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await once(cp, 'exit');
|
|
47
|
+
if (cp.exitCode !== 0)
|
|
48
|
+
throw new Error('npm installation failed with exit code ' + cp.exitCode);
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
fs.rmSync(optPj);
|
|
52
|
+
throw e;
|
|
53
|
+
}
|
|
54
|
+
console.log('native dependencies installed.');
|
|
55
|
+
}
|
|
@@ -4,7 +4,7 @@ import path from 'path';
|
|
|
4
4
|
import { ScryptedNativeId, DeviceManager, Logger, Device, DeviceManifest, DeviceState, EndpointManager, SystemDeviceState, ScryptedStatic, SystemManager, MediaManager, ScryptedMimeTypes, ScryptedInterface, ScryptedInterfaceProperty, HttpRequest } from '@scrypted/sdk/types'
|
|
5
5
|
import { PluginAPI, PluginLogger, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
|
|
6
6
|
import { SystemManagerImpl } from './system';
|
|
7
|
-
import { RpcPeer, RPCResultError } from '../rpc';
|
|
7
|
+
import { RpcPeer, RPCResultError, PROPERTY_PROXY_ONEWAY_METHODS, PROPERTY_JSON_DISABLE_SERIALIZATION } from '../rpc';
|
|
8
8
|
import { BufferSerializer } from './buffer-serializer';
|
|
9
9
|
import { EventEmitter } from 'events';
|
|
10
10
|
import { createWebSocketClass } from './plugin-remote-websocket';
|
|
@@ -285,6 +285,7 @@ export interface PluginRemoteAttachOptions {
|
|
|
285
285
|
getDeviceConsole?: (nativeId?: ScryptedNativeId) => Console;
|
|
286
286
|
getMixinConsole?: (id: string, nativeId?: ScryptedNativeId) => Console;
|
|
287
287
|
events?: EventEmitter;
|
|
288
|
+
beforeLoadZip?: (zip: AdmZip, packageJson: any) => Promise<void>;
|
|
288
289
|
}
|
|
289
290
|
|
|
290
291
|
export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOptions): Promise<ScryptedStatic> {
|
|
@@ -324,9 +325,9 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
|
|
|
324
325
|
|
|
325
326
|
const localStorage = new StorageImpl(deviceManager, undefined);
|
|
326
327
|
|
|
327
|
-
const remote: PluginRemote & {
|
|
328
|
-
|
|
329
|
-
|
|
328
|
+
const remote: PluginRemote & { [PROPERTY_JSON_DISABLE_SERIALIZATION]: boolean, [PROPERTY_PROXY_ONEWAY_METHODS]: string[] } = {
|
|
329
|
+
[PROPERTY_JSON_DISABLE_SERIALIZATION]: true,
|
|
330
|
+
[PROPERTY_PROXY_ONEWAY_METHODS]: [
|
|
330
331
|
'notify',
|
|
331
332
|
'updateDeviceState',
|
|
332
333
|
'setSystemState',
|
|
@@ -401,11 +402,11 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
|
|
|
401
402
|
done(ret);
|
|
402
403
|
},
|
|
403
404
|
|
|
404
|
-
async loadZip(packageJson: any, zipData: Buffer,
|
|
405
|
+
async loadZip(packageJson: any, zipData: Buffer, zipOptions?: PluginRemoteLoadZipOptions) {
|
|
405
406
|
const pluginConsole = getDeviceConsole?.(undefined);
|
|
406
407
|
pluginConsole?.log('starting plugin', pluginId, packageJson.version);
|
|
407
408
|
const zip = new AdmZip(zipData);
|
|
408
|
-
|
|
409
|
+
await options?.beforeLoadZip?.(zip, packageJson);
|
|
409
410
|
const main = zip.getEntry('main.nodejs.js');
|
|
410
411
|
const script = main.getData().toString();
|
|
411
412
|
const window: any = {};
|
|
@@ -484,7 +485,7 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
|
|
|
484
485
|
events?.emit('params', params);
|
|
485
486
|
|
|
486
487
|
try {
|
|
487
|
-
peer.evalLocal(script,
|
|
488
|
+
peer.evalLocal(script, zipOptions?.filename || '/plugin/main.nodejs.js', params);
|
|
488
489
|
events?.emit('plugin', exports.default);
|
|
489
490
|
pluginConsole?.log('plugin successfully loaded');
|
|
490
491
|
return exports.default;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import mkdirp from 'mkdirp';
|
|
3
|
+
|
|
4
|
+
export function ensurePluginVolume(pluginId: string) {
|
|
5
|
+
const volume = path.join(process.cwd(), 'volume');
|
|
6
|
+
const pluginVolume = path.join(volume, 'plugins', pluginId);
|
|
7
|
+
try {
|
|
8
|
+
mkdirp.sync(pluginVolume);
|
|
9
|
+
}
|
|
10
|
+
catch (e) {
|
|
11
|
+
}
|
|
12
|
+
return pluginVolume;
|
|
13
|
+
}
|
package/src/plugin/system.ts
CHANGED
|
@@ -41,11 +41,15 @@ class DeviceProxyHandler implements ProxyHandler<any>, ScryptedDevice {
|
|
|
41
41
|
return new Proxy(() => p, this);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
async
|
|
45
|
-
const method = target();
|
|
44
|
+
async ensureDevice() {
|
|
46
45
|
if (!this.device)
|
|
47
46
|
this.device = await this.systemManager.api.getDeviceById(this.id);
|
|
48
|
-
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async apply(target: any, thisArg: any, argArray?: any) {
|
|
50
|
+
const method = target();
|
|
51
|
+
await this.ensureDevice();
|
|
52
|
+
if (false && method === 'refresh') {
|
|
49
53
|
const name = this.systemManager.state[this.id]?.[ScryptedInterfaceProperty.name].value;
|
|
50
54
|
this.systemManager.log.i(`requested refresh ${name}`);
|
|
51
55
|
}
|
|
@@ -65,6 +69,10 @@ class DeviceProxyHandler implements ProxyHandler<any>, ScryptedDevice {
|
|
|
65
69
|
async setType(type: ScryptedDeviceType): Promise<void> {
|
|
66
70
|
return this.systemManager.api.setDeviceProperty(this.id, ScryptedInterfaceProperty.type, type);
|
|
67
71
|
}
|
|
72
|
+
|
|
73
|
+
async probe(): Promise<boolean> {
|
|
74
|
+
return this.apply(() => 'probe', undefined, []);
|
|
75
|
+
}
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
|
package/src/rpc.ts
CHANGED
|
@@ -22,7 +22,7 @@ interface RpcParam extends RpcMessage {
|
|
|
22
22
|
interface RpcApply extends RpcMessage {
|
|
23
23
|
id: string;
|
|
24
24
|
proxyId: string;
|
|
25
|
-
|
|
25
|
+
args: any[];
|
|
26
26
|
method: string;
|
|
27
27
|
oneway?: boolean;
|
|
28
28
|
}
|
|
@@ -73,6 +73,9 @@ export function handleFunctionInvocations(thiz: ProxyHandler<any>, target: any,
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
export const PROPERTY_PROXY_ONEWAY_METHODS = '__proxy_oneway_methods';
|
|
76
|
+
export const PROPERTY_JSON_DISABLE_SERIALIZATION = '__json_disable_serialization';
|
|
77
|
+
export const PROPERTY_PROXY_PROPERTIES = '__proxy_props';
|
|
78
|
+
export const PROPERTY_JSON_COPY_SERIALIZE_CHILDREN = '__json_copy_serialize_children';
|
|
76
79
|
|
|
77
80
|
class RpcProxy implements ProxyHandler<any> {
|
|
78
81
|
constructor(public peer: RpcPeer,
|
|
@@ -93,10 +96,12 @@ class RpcProxy implements ProxyHandler<any> {
|
|
|
93
96
|
return this.constructorName;
|
|
94
97
|
if (p === '__proxy_peer')
|
|
95
98
|
return this.peer;
|
|
96
|
-
if (p ===
|
|
99
|
+
if (p === PROPERTY_PROXY_PROPERTIES)
|
|
97
100
|
return this.proxyProps;
|
|
98
101
|
if (p === PROPERTY_PROXY_ONEWAY_METHODS)
|
|
99
102
|
return this.proxyOneWayMethods;
|
|
103
|
+
if (p === PROPERTY_JSON_DISABLE_SERIALIZATION || p === PROPERTY_JSON_COPY_SERIALIZE_CHILDREN)
|
|
104
|
+
return;
|
|
100
105
|
if (p === 'then')
|
|
101
106
|
return;
|
|
102
107
|
if (p === 'constructor')
|
|
@@ -120,7 +125,7 @@ class RpcProxy implements ProxyHandler<any> {
|
|
|
120
125
|
type: "apply",
|
|
121
126
|
id: undefined,
|
|
122
127
|
proxyId: this.id,
|
|
123
|
-
|
|
128
|
+
args,
|
|
124
129
|
method,
|
|
125
130
|
};
|
|
126
131
|
|
|
@@ -146,7 +151,7 @@ export class RPCResultError extends Error {
|
|
|
146
151
|
this.name = options?.name;
|
|
147
152
|
}
|
|
148
153
|
if (options?.stack) {
|
|
149
|
-
this.stack = `${peer.peerName}:${peer.selfName}\n${options.stack
|
|
154
|
+
this.stack = `${peer.peerName}:${peer.selfName}\n${cause?.stack || options.stack}`;
|
|
150
155
|
}
|
|
151
156
|
}
|
|
152
157
|
}
|
|
@@ -279,6 +284,16 @@ export class RpcPeer {
|
|
|
279
284
|
deserialize(value: any): any {
|
|
280
285
|
if (!value)
|
|
281
286
|
return value;
|
|
287
|
+
|
|
288
|
+
const copySerializeChildren = value[PROPERTY_JSON_COPY_SERIALIZE_CHILDREN];
|
|
289
|
+
if (copySerializeChildren) {
|
|
290
|
+
const ret: any = {};
|
|
291
|
+
for (const [key, val] of Object.entries(value)) {
|
|
292
|
+
ret[key] = this.deserialize(val);
|
|
293
|
+
}
|
|
294
|
+
return ret;
|
|
295
|
+
}
|
|
296
|
+
|
|
282
297
|
const { __remote_proxy_id, __local_proxy_id, __remote_constructor_name, __serialized_value, __remote_proxy_props, __remote_proxy_oneway_methods } = value;
|
|
283
298
|
if (__remote_proxy_id) {
|
|
284
299
|
const proxy = this.remoteWeakProxies[__remote_proxy_id]?.deref() || this.newProxy(__remote_proxy_id, __remote_constructor_name, __remote_proxy_props, __remote_proxy_oneway_methods);
|
|
@@ -300,7 +315,15 @@ export class RpcPeer {
|
|
|
300
315
|
}
|
|
301
316
|
|
|
302
317
|
serialize(value: any): any {
|
|
303
|
-
if (
|
|
318
|
+
if (value?.[PROPERTY_JSON_COPY_SERIALIZE_CHILDREN] === true) {
|
|
319
|
+
const ret: any = {};
|
|
320
|
+
for (const [key, val] of Object.entries(value)) {
|
|
321
|
+
ret[key] = this.serialize(val);
|
|
322
|
+
}
|
|
323
|
+
return ret;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (!value || (!value[PROPERTY_JSON_DISABLE_SERIALIZATION] && this.transportSafeArgumentTypes.has(value.constructor?.name))) {
|
|
304
327
|
return value;
|
|
305
328
|
}
|
|
306
329
|
|
|
@@ -311,8 +334,8 @@ export class RpcPeer {
|
|
|
311
334
|
const ret: RpcRemoteProxyValue = {
|
|
312
335
|
__remote_proxy_id: proxyId,
|
|
313
336
|
__remote_constructor_name,
|
|
314
|
-
__remote_proxy_props: value?.
|
|
315
|
-
__remote_proxy_oneway_methods: value?.
|
|
337
|
+
__remote_proxy_props: value?.[PROPERTY_PROXY_PROPERTIES],
|
|
338
|
+
__remote_proxy_oneway_methods: value?.[PROPERTY_PROXY_ONEWAY_METHODS],
|
|
316
339
|
}
|
|
317
340
|
return ret;
|
|
318
341
|
}
|
|
@@ -333,8 +356,8 @@ export class RpcPeer {
|
|
|
333
356
|
const ret: RpcRemoteProxyValue = {
|
|
334
357
|
__remote_proxy_id: undefined,
|
|
335
358
|
__remote_constructor_name,
|
|
336
|
-
__remote_proxy_props: value?.
|
|
337
|
-
__remote_proxy_oneway_methods: value?.
|
|
359
|
+
__remote_proxy_props: value?.[PROPERTY_PROXY_PROPERTIES],
|
|
360
|
+
__remote_proxy_oneway_methods: value?.[PROPERTY_PROXY_ONEWAY_METHODS],
|
|
338
361
|
__serialized_value: serialized,
|
|
339
362
|
}
|
|
340
363
|
return ret;
|
|
@@ -347,8 +370,8 @@ export class RpcPeer {
|
|
|
347
370
|
const ret: RpcRemoteProxyValue = {
|
|
348
371
|
__remote_proxy_id: proxyId,
|
|
349
372
|
__remote_constructor_name,
|
|
350
|
-
__remote_proxy_props: value?.
|
|
351
|
-
__remote_proxy_oneway_methods: value?.
|
|
373
|
+
__remote_proxy_props: value?.[PROPERTY_PROXY_PROPERTIES],
|
|
374
|
+
__remote_proxy_oneway_methods: value?.[PROPERTY_PROXY_ONEWAY_METHODS],
|
|
352
375
|
}
|
|
353
376
|
|
|
354
377
|
return ret;
|
|
@@ -391,7 +414,7 @@ export class RpcPeer {
|
|
|
391
414
|
throw new Error(`proxy id ${rpcApply.proxyId} not found`);
|
|
392
415
|
|
|
393
416
|
const args = [];
|
|
394
|
-
for (const arg of (rpcApply.
|
|
417
|
+
for (const arg of (rpcApply.args || [])) {
|
|
395
418
|
args.push(this.deserialize(arg));
|
|
396
419
|
}
|
|
397
420
|
|
package/src/runtime.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Level } from './level';
|
|
2
|
-
import {
|
|
2
|
+
import { PluginHost } from './plugin/plugin-host';
|
|
3
3
|
import cluster from 'cluster';
|
|
4
4
|
import { ScryptedNativeId, Device, EngineIOHandler, HttpRequest, HttpRequestHandler, OauthClient, PushHandler, ScryptedDevice, ScryptedInterface, ScryptedInterfaceProperty } from '@scrypted/sdk/types';
|
|
5
5
|
import { PluginDeviceProxyHandler } from './plugin/plugin-device';
|
|
@@ -32,13 +32,14 @@ import {spawn as ptySpawn} from 'node-pty';
|
|
|
32
32
|
import child_process from 'child_process';
|
|
33
33
|
import fs from 'fs';
|
|
34
34
|
import path from 'path';
|
|
35
|
+
import { ensurePluginVolume } from './plugin/plugin-volume';
|
|
35
36
|
|
|
36
37
|
interface DeviceProxyPair {
|
|
37
38
|
handler: PluginDeviceProxyHandler;
|
|
38
39
|
proxy: ScryptedDevice;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
const MIN_SCRYPTED_CORE_VERSION = '
|
|
42
|
+
const MIN_SCRYPTED_CORE_VERSION = 'v0.0.146';
|
|
42
43
|
const PLUGIN_DEVICE_STATE_VERSION = 2;
|
|
43
44
|
|
|
44
45
|
export class ScryptedRuntime {
|
|
@@ -427,33 +428,6 @@ export class ScryptedRuntime {
|
|
|
427
428
|
return proxyPair;
|
|
428
429
|
}
|
|
429
430
|
|
|
430
|
-
async installOptionalDependencies(packageJson: any, currentPackageJson: any) {
|
|
431
|
-
const { optionalDependencies } = packageJson;
|
|
432
|
-
if (!optionalDependencies)
|
|
433
|
-
return;
|
|
434
|
-
if (!Object.keys(packageJson).length)
|
|
435
|
-
return;
|
|
436
|
-
const currentOptionalDependencies = currentPackageJson?.optionalDependencies || {};
|
|
437
|
-
|
|
438
|
-
if (JSON.stringify(optionalDependencies) === JSON.stringify(currentOptionalDependencies))
|
|
439
|
-
return;
|
|
440
|
-
|
|
441
|
-
const reduced = Object.assign({}, packageJson);
|
|
442
|
-
reduced.dependencies = reduced.optionalDependencies;
|
|
443
|
-
delete reduced.optionalDependencies;
|
|
444
|
-
delete reduced.devDependencies;
|
|
445
|
-
|
|
446
|
-
const pluginVolume = ensurePluginVolume(reduced.name);
|
|
447
|
-
const optPj = path.join(pluginVolume, 'package.json');
|
|
448
|
-
fs.writeFileSync(optPj, JSON.stringify(reduced));
|
|
449
|
-
const cp = child_process.spawn('npm', ['--prefix', pluginVolume, 'install'], {
|
|
450
|
-
cwd: pluginVolume,
|
|
451
|
-
stdio: 'inherit',
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
await once(cp, 'exit');
|
|
455
|
-
}
|
|
456
|
-
|
|
457
431
|
async installNpm(pkg: string, version?: string): Promise<PluginHost> {
|
|
458
432
|
const registry = (await axios(`https://registry.npmjs.org/${pkg}`)).data;
|
|
459
433
|
if (!version) {
|
|
@@ -488,7 +462,6 @@ export class ScryptedRuntime {
|
|
|
488
462
|
const packageJson = JSON.parse(packageJsonEntry.toString());
|
|
489
463
|
const npmPackage = packageJson.name;
|
|
490
464
|
const plugin = await this.datastore.tryGet(Plugin, npmPackage) || new Plugin();
|
|
491
|
-
await this.installOptionalDependencies(packageJson, plugin.packageJson);
|
|
492
465
|
|
|
493
466
|
plugin._id = npmPackage;
|
|
494
467
|
plugin.packageJson = packageJson;
|
|
@@ -542,6 +515,27 @@ export class ScryptedRuntime {
|
|
|
542
515
|
}
|
|
543
516
|
|
|
544
517
|
const pluginHost = new PluginHost(this, plugin, pluginDebug);
|
|
518
|
+
pluginHost.worker.once('exit', () => {
|
|
519
|
+
if (pluginHost.killed)
|
|
520
|
+
return;
|
|
521
|
+
pluginHost.kill();
|
|
522
|
+
console.error('plugin unexpectedly exited, restarting in 1 minute', pluginHost.pluginId);
|
|
523
|
+
setTimeout(async () => {
|
|
524
|
+
const existing = this.plugins[pluginHost.pluginId];
|
|
525
|
+
if (existing !== pluginHost) {
|
|
526
|
+
console.log('scheduled plugin restart cancelled, plugin was restarted by user', pluginHost.pluginId);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const plugin = await this.datastore.tryGet(Plugin, pluginHost.pluginId);
|
|
531
|
+
if (!plugin) {
|
|
532
|
+
console.log('scheduled plugin restart cancelled, plugin no longer exists', pluginHost.pluginId);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
this.runPlugin(plugin).catch(e => console.error('error restarting plugin', plugin._id, e));
|
|
537
|
+
}, 60000);
|
|
538
|
+
})
|
|
545
539
|
this.plugins[plugin._id] = pluginHost;
|
|
546
540
|
|
|
547
541
|
return pluginHost;
|
package/src/scrypted-main.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import process from 'process';
|
|
3
|
-
import pem from 'pem';
|
|
4
|
-
import { CertificateCreationResult } from 'pem';
|
|
5
3
|
import http from 'http';
|
|
6
4
|
import https from 'https';
|
|
7
5
|
import express from 'express';
|
|
@@ -26,12 +24,12 @@ import semver from 'semver';
|
|
|
26
24
|
import { Info } from './services/info';
|
|
27
25
|
import { getAddresses } from './addresses';
|
|
28
26
|
import { sleep } from './sleep';
|
|
27
|
+
import { createSelfSignedCertificate, CURRENT_SELF_SIGNED_CERTIFICATE_VERSION } from './cert';
|
|
29
28
|
|
|
30
29
|
if (!semver.gte(process.version, '16.0.0')) {
|
|
31
30
|
throw new Error('"node" version out of date. Please update node to v16 or higher.')
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
|
|
35
33
|
process.on('unhandledRejection', error => {
|
|
36
34
|
if (error?.constructor !== RPCResultError) {
|
|
37
35
|
throw error;
|
|
@@ -110,18 +108,6 @@ else {
|
|
|
110
108
|
// parse some custom thing into a Buffer
|
|
111
109
|
app.use(bodyParser.raw({ type: 'application/zip', limit: 100000000 }) as any)
|
|
112
110
|
|
|
113
|
-
async function createCertificate(options: pem.CertificateCreationOptions): Promise<pem.CertificateCreationResult> {
|
|
114
|
-
return new Promise((resolve, reject) => {
|
|
115
|
-
pem.createCertificate(options, (err: Error, keys: CertificateCreationResult) => {
|
|
116
|
-
if (err) {
|
|
117
|
-
reject(err);
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
resolve(keys);
|
|
121
|
-
})
|
|
122
|
-
})
|
|
123
|
-
}
|
|
124
|
-
|
|
125
111
|
async function start() {
|
|
126
112
|
const volumeDir = process.env.SCRYPTED_VOLUME || path.join(process.cwd(), 'volume');
|
|
127
113
|
mkdirp.sync(volumeDir);
|
|
@@ -136,11 +122,8 @@ else {
|
|
|
136
122
|
|
|
137
123
|
let certSetting = await db.tryGet(Settings, 'certificate') as Settings;
|
|
138
124
|
|
|
139
|
-
if (
|
|
140
|
-
const cert =
|
|
141
|
-
selfSigned: true,
|
|
142
|
-
});
|
|
143
|
-
|
|
125
|
+
if (certSetting?.value?.version !== CURRENT_SELF_SIGNED_CERTIFICATE_VERSION) {
|
|
126
|
+
const cert = createSelfSignedCertificate();
|
|
144
127
|
|
|
145
128
|
certSetting = new Settings();
|
|
146
129
|
certSetting._id = 'certificate';
|
|
@@ -167,6 +150,7 @@ else {
|
|
|
167
150
|
});
|
|
168
151
|
|
|
169
152
|
const keys = certSetting.value;
|
|
153
|
+
|
|
170
154
|
const secure = https.createServer({ key: keys.serviceKey, cert: keys.certificate }, app);
|
|
171
155
|
listenServerPort('SCRYPTED_SECURE_PORT', SCRYPTED_SECURE_PORT, secure);
|
|
172
156
|
const insecure = http.createServer(app);
|
|
@@ -193,7 +177,7 @@ else {
|
|
|
193
177
|
});
|
|
194
178
|
|
|
195
179
|
// use a hash of the private key as the cookie secret.
|
|
196
|
-
app.use(cookieParser(crypto.createHash('sha256').update(certSetting.value.
|
|
180
|
+
app.use(cookieParser(crypto.createHash('sha256').update(certSetting.value.serviceKey).digest().toString('hex')));
|
|
197
181
|
|
|
198
182
|
app.all('*', async (req, res, next) => {
|
|
199
183
|
// this is a trap for all auth.
|
|
@@ -316,9 +300,6 @@ else {
|
|
|
316
300
|
const npmPackage = req.query.npmPackage as string;
|
|
317
301
|
const plugin = await db.tryGet(Plugin, npmPackage) || new Plugin();
|
|
318
302
|
|
|
319
|
-
const packageJson = req.body;
|
|
320
|
-
await scrypted.installOptionalDependencies(packageJson, plugin.packageJson);
|
|
321
|
-
|
|
322
303
|
plugin._id = npmPackage;
|
|
323
304
|
plugin.packageJson = req.body;
|
|
324
305
|
|