@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.

Files changed (42) hide show
  1. package/.vscode/settings.json +2 -1
  2. package/dist/cert.js +75 -0
  3. package/dist/cert.js.map +1 -0
  4. package/dist/plugin/media.js +50 -14
  5. package/dist/plugin/media.js.map +1 -1
  6. package/dist/plugin/plugin-device.js +11 -1
  7. package/dist/plugin/plugin-device.js.map +1 -1
  8. package/dist/plugin/plugin-host-api.js +7 -3
  9. package/dist/plugin/plugin-host-api.js.map +1 -1
  10. package/dist/plugin/plugin-host.js +66 -45
  11. package/dist/plugin/plugin-host.js.map +1 -1
  12. package/dist/plugin/plugin-npm-dependencies.js +53 -0
  13. package/dist/plugin/plugin-npm-dependencies.js.map +1 -0
  14. package/dist/plugin/plugin-remote.js +5 -5
  15. package/dist/plugin/plugin-remote.js.map +1 -1
  16. package/dist/plugin/plugin-volume.js +20 -0
  17. package/dist/plugin/plugin-volume.js.map +1 -0
  18. package/dist/plugin/system.js +9 -3
  19. package/dist/plugin/system.js.map +1 -1
  20. package/dist/rpc.js +32 -12
  21. package/dist/rpc.js.map +1 -1
  22. package/dist/runtime.js +20 -27
  23. package/dist/runtime.js.map +1 -1
  24. package/dist/scrypted-main.js +4 -19
  25. package/dist/scrypted-main.js.map +1 -1
  26. package/package.json +4 -3
  27. package/python/media.py +40 -0
  28. package/python/plugin-remote.py +67 -30
  29. package/python/rpc.py +24 -81
  30. package/src/cert.ts +74 -0
  31. package/src/plugin/media.ts +53 -14
  32. package/src/plugin/plugin-api.ts +0 -1
  33. package/src/plugin/plugin-device.ts +13 -2
  34. package/src/plugin/plugin-host-api.ts +15 -3
  35. package/src/plugin/plugin-host.ts +69 -49
  36. package/src/plugin/plugin-npm-dependencies.ts +55 -0
  37. package/src/plugin/plugin-remote.ts +8 -7
  38. package/src/plugin/plugin-volume.ts +13 -0
  39. package/src/plugin/system.ts +11 -3
  40. package/src/rpc.ts +35 -12
  41. package/src/runtime.ts +24 -30
  42. 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
- export function ensurePluginVolume(pluginId: string) {
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
- this.api = new PluginHostAPI(scrypted, plugin, this);
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
- process.stdout.write(data);
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.remote)
669
- await this.remoteReadyPromise;
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.remote)
674
- await this.remoteReadyPromise;
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 & { __proxy_required: boolean } = {
328
- __proxy_required: true,
329
- __proxy_oneway_methods: [
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, options?: PluginRemoteLoadZipOptions) {
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
- events?.emit('zip', zip, pluginId);
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, options?.filename || '/plugin/main.nodejs.js', params);
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
+ }
@@ -41,11 +41,15 @@ class DeviceProxyHandler implements ProxyHandler<any>, ScryptedDevice {
41
41
  return new Proxy(() => p, this);
42
42
  }
43
43
 
44
- async apply(target: any, thisArg: any, argArray?: any) {
45
- const method = target();
44
+ async ensureDevice() {
46
45
  if (!this.device)
47
46
  this.device = await this.systemManager.api.getDeviceById(this.id);
48
- if (method === 'refresh') {
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
- argArray: any;
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 === '__proxy_props')
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
- argArray: args,
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.split('\n').slice(1).join('\n')}`;
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 (!value || (!value.__proxy_required && this.transportSafeArgumentTypes.has(value.constructor?.name))) {
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?.__proxy_props,
315
- __remote_proxy_oneway_methods: value?.__proxy_oneway_methods,
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?.__proxy_props,
337
- __remote_proxy_oneway_methods: value?.__proxy_oneway_methods,
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?.__proxy_props,
351
- __remote_proxy_oneway_methods: value?.__proxy_oneway_methods,
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.argArray || [])) {
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 { ensurePluginVolume, PluginHost } from './plugin/plugin-host';
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 = '0.0.135';
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;
@@ -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 (!certSetting) {
140
- const cert = await createCertificate({
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.clientKey).digest().toString('hex')));
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