@scrypted/server 0.0.115 → 0.0.120

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