@scrypted/server 0.0.114 → 0.0.119

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 (55) 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 +25 -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 -226
  13. package/dist/plugin/plugin-host.js.map +1 -1
  14. package/dist/plugin/plugin-lazy-remote.js.map +1 -1
  15. package/dist/plugin/plugin-remote-worker.js +252 -0
  16. package/dist/plugin/plugin-remote-worker.js.map +1 -0
  17. package/dist/plugin/plugin-remote.js +39 -15
  18. package/dist/plugin/plugin-remote.js.map +1 -1
  19. package/dist/plugin/plugin-repl.js +3 -1
  20. package/dist/plugin/plugin-repl.js.map +1 -1
  21. package/dist/plugin/system.js +11 -6
  22. package/dist/plugin/system.js.map +1 -1
  23. package/dist/rpc.js +22 -3
  24. package/dist/rpc.js.map +1 -1
  25. package/dist/runtime.js +19 -4
  26. package/dist/runtime.js.map +1 -1
  27. package/dist/scrypted-main.js +4 -437
  28. package/dist/scrypted-main.js.map +1 -1
  29. package/dist/scrypted-plugin-main.js +8 -0
  30. package/dist/scrypted-plugin-main.js.map +1 -0
  31. package/dist/scrypted-server-main.js +448 -0
  32. package/dist/scrypted-server-main.js.map +1 -0
  33. package/package.json +7 -6
  34. package/python/media.py +1 -12
  35. package/python/plugin-remote.py +22 -7
  36. package/python/rpc.py +5 -4
  37. package/src/http-interfaces.ts +12 -5
  38. package/src/level.ts +0 -2
  39. package/src/plugin/media.ts +3 -3
  40. package/src/plugin/plugin-api.ts +9 -1
  41. package/src/plugin/plugin-console.ts +0 -1
  42. package/src/plugin/plugin-device.ts +26 -2
  43. package/src/plugin/plugin-host-api.ts +2 -2
  44. package/src/plugin/plugin-host.ts +121 -252
  45. package/src/plugin/plugin-http.ts +2 -2
  46. package/src/plugin/plugin-lazy-remote.ts +1 -1
  47. package/src/plugin/plugin-remote-worker.ts +271 -0
  48. package/src/plugin/plugin-remote.ts +46 -17
  49. package/src/plugin/plugin-repl.ts +4 -2
  50. package/src/plugin/system.ts +15 -13
  51. package/src/rpc.ts +28 -4
  52. package/src/runtime.ts +19 -4
  53. package/src/scrypted-main.ts +5 -508
  54. package/src/scrypted-plugin-main.ts +6 -0
  55. package/src/scrypted-server-main.ts +516 -0
@@ -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;
@@ -1,6 +1,6 @@
1
1
  import { EventListenerOptions, EventDetails, EventListenerRegister, ScryptedDevice, ScryptedInterface, ScryptedInterfaceDescriptors, SystemDeviceState, SystemManager, ScryptedInterfaceProperty, ScryptedDeviceType, Logger } from "@scrypted/sdk/types";
2
2
  import { PluginAPI } from "./plugin-api";
3
- import { handleFunctionInvocations, PROPERTY_PROXY_ONEWAY_METHODS } from '../rpc';
3
+ import { handleFunctionInvocations, PrimitiveProxyHandler, PROPERTY_PROXY_ONEWAY_METHODS } from '../rpc';
4
4
  import { EventRegistry } from "../event-registry";
5
5
  import { allInterfaceProperties, isValidInterfaceMethod } from "./descriptor";
6
6
 
@@ -11,13 +11,13 @@ function newDeviceProxy(id: string, systemManager: SystemManagerImpl) {
11
11
  }
12
12
 
13
13
 
14
- class DeviceProxyHandler implements ProxyHandler<any>, ScryptedDevice {
15
- device: ScryptedDevice;
16
- id: string;
17
- systemManager: SystemManagerImpl;
18
- constructor(id: string, systemManager: SystemManagerImpl) {
19
- this.id = id;
20
- this.systemManager = systemManager;
14
+ class DeviceProxyHandler implements PrimitiveProxyHandler<any>, ScryptedDevice {
15
+ device: Promise<ScryptedDevice>;
16
+ constructor(public id: string, public systemManager: SystemManagerImpl) {
17
+ }
18
+
19
+ toPrimitive() {
20
+ return `ScryptedDevice-${this.id}`
21
21
  }
22
22
 
23
23
  get(target: any, p: PropertyKey, receiver: any): any {
@@ -41,19 +41,20 @@ class DeviceProxyHandler implements ProxyHandler<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
  }
@@ -63,7 +73,7 @@ interface Deferred {
63
73
  reject: any;
64
74
  }
65
75
 
66
- export function handleFunctionInvocations(thiz: ProxyHandler<any>, target: any, p: PropertyKey, receiver: any): any {
76
+ export function handleFunctionInvocations(thiz: PrimitiveProxyHandler<any>, target: any, p: PropertyKey, receiver: any): any {
67
77
  if (p === 'apply') {
68
78
  return (thisArg: any, args: any[]) => {
69
79
  return thiz.apply(target, thiz, args);
@@ -74,6 +84,11 @@ export function handleFunctionInvocations(thiz: ProxyHandler<any>, target: any,
74
84
  return thiz.apply(target, thiz, args);
75
85
  }
76
86
  }
87
+ else if (p === 'toString' || p === Symbol.toPrimitive) {
88
+ return (thisArg: any, ...args: any[]) => {
89
+ return thiz.toPrimitive();
90
+ }
91
+ }
77
92
  }
78
93
 
79
94
  export const PROPERTY_PROXY_ONEWAY_METHODS = '__proxy_oneway_methods';
@@ -81,8 +96,11 @@ export const PROPERTY_JSON_DISABLE_SERIALIZATION = '__json_disable_serialization
81
96
  export const PROPERTY_PROXY_PROPERTIES = '__proxy_props';
82
97
  export const PROPERTY_JSON_COPY_SERIALIZE_CHILDREN = '__json_copy_serialize_children';
83
98
 
84
- class RpcProxy implements ProxyHandler<any> {
99
+ export interface PrimitiveProxyHandler<T extends object> extends ProxyHandler<T> {
100
+ toPrimitive(): any;
101
+ }
85
102
 
103
+ class RpcProxy implements PrimitiveProxyHandler<any> {
86
104
  constructor(public peer: RpcPeer,
87
105
  public entry: LocalProxiedEntry,
88
106
  public constructorName: string,
@@ -90,6 +108,11 @@ class RpcProxy implements ProxyHandler<any> {
90
108
  public proxyOneWayMethods: string[]) {
91
109
  }
92
110
 
111
+ toPrimitive() {
112
+ const peer = this.peer;
113
+ return `RpcProxy-${peer.selfName}:${peer.peerName}: ${this.constructorName}`;
114
+ }
115
+
93
116
  get(target: any, p: PropertyKey, receiver: any): any {
94
117
  if (p === '__proxy_id')
95
118
  return this.entry.id;
@@ -122,6 +145,9 @@ class RpcProxy implements ProxyHandler<any> {
122
145
  }
123
146
 
124
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
+
125
151
  // rpc objects can be functions. if the function is a oneway method,
126
152
  // it will have a null in the oneway method list. this is because
127
153
  // undefined is not JSON serializable.
@@ -416,7 +442,6 @@ export class RpcPeer {
416
442
  const weakref = new WeakRef(proxy);
417
443
  this.remoteWeakProxies[proxyId] = weakref;
418
444
  this.finalizers.register(rpc, localProxiedEntry);
419
- global.gc?.();
420
445
  return proxy;
421
446
  }
422
447
 
@@ -496,7 +521,6 @@ export class RpcPeer {
496
521
  const localProxiedEntry = this.localProxied.get(local);
497
522
  // if a finalizer id is specified, it must match.
498
523
  if (rpcFinalize.__local_proxy_finalizer_id && rpcFinalize.__local_proxy_finalizer_id !== localProxiedEntry?.finalizerId) {
499
- console.error(this.selfName, this.peerName, 'finalizer mismatch')
500
524
  break;
501
525
  }
502
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,7 +460,21 @@ 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