@scrypted/server 0.1.15 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @scrypted/server might be problematic. Click here for more details.
- package/dist/event-registry.js +3 -4
- package/dist/event-registry.js.map +1 -1
- package/dist/http-interfaces.js +11 -0
- package/dist/http-interfaces.js.map +1 -1
- package/dist/plugin/media.js +77 -66
- package/dist/plugin/media.js.map +1 -1
- package/dist/plugin/plugin-api.js +1 -1
- package/dist/plugin/plugin-api.js.map +1 -1
- package/dist/plugin/plugin-device.js +25 -11
- package/dist/plugin/plugin-device.js.map +1 -1
- package/dist/plugin/plugin-host-api.js.map +1 -1
- package/dist/plugin/plugin-host.js +11 -6
- package/dist/plugin/plugin-host.js.map +1 -1
- package/dist/plugin/plugin-http.js +1 -1
- package/dist/plugin/plugin-http.js.map +1 -1
- package/dist/plugin/plugin-remote-worker.js +170 -17
- package/dist/plugin/plugin-remote-worker.js.map +1 -1
- package/dist/plugin/plugin-remote.js +25 -85
- package/dist/plugin/plugin-remote.js.map +1 -1
- package/dist/plugin/runtime/node-fork-worker.js +11 -3
- package/dist/plugin/runtime/node-fork-worker.js.map +1 -1
- package/dist/plugin/socket-serializer.js +17 -0
- package/dist/plugin/socket-serializer.js.map +1 -0
- package/dist/rpc.js +3 -3
- package/dist/rpc.js.map +1 -1
- package/dist/runtime.js +14 -11
- package/dist/runtime.js.map +1 -1
- package/dist/scrypted-plugin-main.js +4 -1
- package/dist/scrypted-plugin-main.js.map +1 -1
- package/dist/scrypted-server-main.js +53 -12
- package/dist/scrypted-server-main.js.map +1 -1
- package/dist/server-settings.js +5 -1
- package/dist/server-settings.js.map +1 -1
- package/dist/state.js +2 -1
- package/dist/state.js.map +1 -1
- package/package.json +5 -11
- package/src/event-registry.ts +3 -4
- package/src/http-interfaces.ts +13 -0
- package/src/plugin/media.ts +93 -74
- package/src/plugin/plugin-api.ts +5 -4
- package/src/plugin/plugin-device.ts +25 -11
- package/src/plugin/plugin-host-api.ts +1 -1
- package/src/plugin/plugin-host.ts +6 -5
- package/src/plugin/plugin-http.ts +2 -2
- package/src/plugin/plugin-remote-worker.ts +211 -23
- package/src/plugin/plugin-remote.ts +31 -94
- package/src/plugin/runtime/node-fork-worker.ts +11 -3
- package/src/plugin/runtime/runtime-worker.ts +1 -1
- package/src/plugin/socket-serializer.ts +15 -0
- package/src/rpc.ts +3 -2
- package/src/runtime.ts +10 -10
- package/src/scrypted-plugin-main.ts +4 -1
- package/src/scrypted-server-main.ts +59 -13
- package/src/state.ts +2 -1
@@ -216,15 +216,24 @@ export class PluginDeviceProxyHandler implements PrimitiveProxyHandler<any>, Scr
|
|
216
216
|
let { allInterfaces } = await previousEntry;
|
217
217
|
try {
|
218
218
|
const mixinProvider = this.scrypted.getDevice(mixinId) as ScryptedDevice & MixinProvider;
|
219
|
-
const
|
219
|
+
const isMixinProvider = mixinProvider?.interfaces?.includes(ScryptedInterface.MixinProvider);
|
220
|
+
const interfaces = isMixinProvider && await mixinProvider?.canMixin(type, allInterfaces) as any as ScryptedInterface[];
|
220
221
|
if (!interfaces) {
|
221
222
|
// this is not an error
|
222
223
|
// do not advertise interfaces so it is skipped during
|
223
224
|
// vtable lookup.
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
225
|
+
if (!mixinProvider || (isMixinProvider && !interfaces)) {
|
226
|
+
console.log(`Mixin provider ${mixinId} can no longer mixin ${this.id}. Removing.`, {
|
227
|
+
mixinProvider: !!mixinProvider,
|
228
|
+
interfaces,
|
229
|
+
});
|
230
|
+
const mixins: string[] = getState(pluginDevice, ScryptedInterfaceProperty.mixins) || [];
|
231
|
+
this.scrypted.stateManager.setPluginDeviceState(pluginDevice, ScryptedInterfaceProperty.mixins, mixins.filter(mid => mid !== mixinId))
|
232
|
+
this.scrypted.datastore.upsert(pluginDevice);
|
233
|
+
}
|
234
|
+
else {
|
235
|
+
console.log(`Mixin provider ${mixinId} can not mixin ${this.id}. It is no longer a MixinProvider. This may be temporary. Passing through.`);
|
236
|
+
}
|
228
237
|
return {
|
229
238
|
passthrough: true,
|
230
239
|
allInterfaces,
|
@@ -353,12 +362,17 @@ export class PluginDeviceProxyHandler implements PrimitiveProxyHandler<any>, Scr
|
|
353
362
|
for (const mixin of this.mixinTable) {
|
354
363
|
const entry = await mixin.entry;
|
355
364
|
if (!entry.methods) {
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
365
|
+
if (entry.interfaces.size) {
|
366
|
+
const pluginDevice = this.scrypted.findPluginDeviceById(mixin.mixinProviderId || this.id);
|
367
|
+
const plugin = this.scrypted.plugins[pluginDevice.pluginId];
|
368
|
+
let methods = new Set<string>(getInterfaceMethods(ScryptedInterfaceDescriptors, entry.interfaces))
|
369
|
+
if (plugin.api.descriptors)
|
370
|
+
methods = new Set<string>([...methods, ...getInterfaceMethods(plugin.api.descriptors, entry.interfaces)]);
|
371
|
+
entry.methods = methods;
|
372
|
+
}
|
373
|
+
else {
|
374
|
+
entry.methods = new Set();
|
375
|
+
}
|
362
376
|
}
|
363
377
|
if (entry.methods.has(method)) {
|
364
378
|
return { mixin, entry };
|
@@ -47,7 +47,7 @@ export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAP
|
|
47
47
|
|
48
48
|
// do we care about mixin validation here?
|
49
49
|
// maybe to prevent/notify errant dangling events?
|
50
|
-
async onMixinEvent(id: string, nativeIdOrMixinDevice: ScryptedNativeId | any, eventInterface:
|
50
|
+
async onMixinEvent(id: string, nativeIdOrMixinDevice: ScryptedNativeId | any, eventInterface: string, eventData?: any) {
|
51
51
|
// nativeId code path has been deprecated in favor of mixin object 12/10/2021
|
52
52
|
const device = this.scrypted.findPluginDeviceById(id);
|
53
53
|
|
@@ -135,7 +135,7 @@ export class PluginHost {
|
|
135
135
|
this.io.on('connection', async (socket) => {
|
136
136
|
try {
|
137
137
|
try {
|
138
|
-
if (socket.request.url.indexOf('/api') !== -1) {
|
138
|
+
if (socket.request.url.indexOf('/engine.io/api') !== -1) {
|
139
139
|
if (socket.request.url.indexOf('/public') !== -1) {
|
140
140
|
socket.close();
|
141
141
|
return;
|
@@ -179,7 +179,7 @@ export class PluginHost {
|
|
179
179
|
|
180
180
|
const { runtime } = this.packageJson.scrypted;
|
181
181
|
const mediaManager = runtime === 'python'
|
182
|
-
? new MediaManagerHostImpl(pluginDeviceId, scrypted.stateManager.getSystemState(), console, id => scrypted.getDevice(id))
|
182
|
+
? new MediaManagerHostImpl(pluginDeviceId, () => scrypted.stateManager.getSystemState(), console, id => scrypted.getDevice(id))
|
183
183
|
: undefined;
|
184
184
|
|
185
185
|
this.api = new PluginHostAPI(scrypted, this.pluginId, this, mediaManager);
|
@@ -214,7 +214,6 @@ export class PluginHost {
|
|
214
214
|
await remote.setNativeId(pluginDevice.nativeId, pluginDevice._id, pluginDevice.storage || {});
|
215
215
|
}
|
216
216
|
|
217
|
-
await remote.setSystemState(scrypted.stateManager.getSystemState());
|
218
217
|
const waitDebug = pluginDebug?.waitDebug;
|
219
218
|
if (waitDebug) {
|
220
219
|
console.info('waiting for debugger...');
|
@@ -374,7 +373,8 @@ export class PluginHost {
|
|
374
373
|
serializer.setupRpcPeer(rpcPeer);
|
375
374
|
|
376
375
|
// wrap the host api with a connection specific api that can be torn down on disconnect
|
377
|
-
const
|
376
|
+
const createMediaManager = await this.peer.getParam('createMediaManager');
|
377
|
+
const api = new PluginAPIProxy(this.api, await createMediaManager());
|
378
378
|
const kill = () => {
|
379
379
|
serializer.onDisconnected();
|
380
380
|
api.removeListeners();
|
@@ -389,7 +389,8 @@ export class PluginHost {
|
|
389
389
|
const rpcPeer = createDuplexRpcPeer(`api/${this.pluginId}`, 'duplex', duplex, duplex);
|
390
390
|
|
391
391
|
// wrap the host api with a connection specific api that can be torn down on disconnect
|
392
|
-
const
|
392
|
+
const createMediaManager = await this.peer.getParam('createMediaManager');
|
393
|
+
const api = new PluginAPIProxy(this.api, await createMediaManager());
|
393
394
|
const kill = () => {
|
394
395
|
api.removeListeners();
|
395
396
|
};
|
@@ -38,7 +38,7 @@ export abstract class PluginHttp<T> {
|
|
38
38
|
|
39
39
|
abstract handleEngineIOEndpoint(req: Request, res: ServerResponse, endpointRequest: HttpRequest, pluginData: T): void;
|
40
40
|
abstract handleRequestEndpoint(req: Request, res: Response, endpointRequest: HttpRequest, pluginData: T): void;
|
41
|
-
abstract getEndpointPluginData(endpoint: string, isUpgrade: boolean, isEngineIOEndpoint: boolean): Promise<T>;
|
41
|
+
abstract getEndpointPluginData(req: Request, endpoint: string, isUpgrade: boolean, isEngineIOEndpoint: boolean): Promise<T>;
|
42
42
|
abstract handleWebSocket(endpoint: string, httpRequest: HttpRequest, ws: WebSocket, pluginData: T): Promise<void>;
|
43
43
|
|
44
44
|
async endpointHandler(req: Request, res: Response, isPublicEndpoint: boolean, isEngineIOEndpoint: boolean,
|
@@ -75,7 +75,7 @@ export abstract class PluginHttp<T> {
|
|
75
75
|
return;
|
76
76
|
}
|
77
77
|
|
78
|
-
const pluginData = await this.getEndpointPluginData(endpoint, isUpgrade, isEngineIOEndpoint);
|
78
|
+
const pluginData = await this.getEndpointPluginData(req, endpoint, isUpgrade, isEngineIOEndpoint);
|
79
79
|
if (!pluginData) {
|
80
80
|
end(404, 'Not Found');
|
81
81
|
return;
|
@@ -1,20 +1,32 @@
|
|
1
1
|
import { DeviceManager, ScryptedNativeId, ScryptedStatic, SystemManager } from '@scrypted/types';
|
2
|
+
import AdmZip from 'adm-zip';
|
2
3
|
import { Console } from 'console';
|
4
|
+
import fs from 'fs';
|
5
|
+
import { Volume } from 'memfs';
|
3
6
|
import net from 'net';
|
7
|
+
import path from 'path';
|
4
8
|
import { install as installSourceMapSupport } from 'source-map-support';
|
5
9
|
import { PassThrough } from 'stream';
|
6
10
|
import { RpcMessage, RpcPeer } from '../rpc';
|
7
11
|
import { MediaManagerImpl } from './media';
|
8
|
-
import { PluginAPI } from './plugin-api';
|
12
|
+
import { PluginAPI, PluginAPIProxy, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
|
9
13
|
import { installOptionalDependencies } from './plugin-npm-dependencies';
|
10
|
-
import { attachPluginRemote, PluginReader } from './plugin-remote';
|
14
|
+
import { attachPluginRemote, DeviceManagerImpl, PluginReader, setupPluginRemote } from './plugin-remote';
|
11
15
|
import { createREPLServer } from './plugin-repl';
|
16
|
+
import { NodeThreadWorker } from './runtime/node-thread-worker';
|
17
|
+
const { link } = require('linkfs');
|
12
18
|
|
13
|
-
|
19
|
+
interface PluginStats {
|
20
|
+
type: 'stats',
|
21
|
+
cpu: NodeJS.CpuUsage;
|
22
|
+
memoryUsage: NodeJS.MemoryUsage;
|
23
|
+
}
|
24
|
+
|
25
|
+
export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessage, reject?: (e: Error) => void, serializationContext?: any) => void) {
|
14
26
|
const peer = new RpcPeer('unknown', 'host', peerSend);
|
15
27
|
|
16
28
|
let systemManager: SystemManager;
|
17
|
-
let deviceManager:
|
29
|
+
let deviceManager: DeviceManagerImpl;
|
18
30
|
let api: PluginAPI;
|
19
31
|
|
20
32
|
const getConsole = (hook: (stdout: PassThrough, stderr: PassThrough) => Promise<void>,
|
@@ -66,13 +78,18 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
|
|
66
78
|
return pluginsPromise;
|
67
79
|
}
|
68
80
|
|
81
|
+
const deviceConsoles = new Map<string, Console>();
|
69
82
|
const getDeviceConsole = (nativeId?: ScryptedNativeId) => {
|
70
83
|
// the the plugin console is simply the default console
|
71
84
|
// and gets read from stderr/stdout.
|
72
85
|
if (!nativeId)
|
73
86
|
return console;
|
74
87
|
|
75
|
-
|
88
|
+
let ret = deviceConsoles.get(nativeId);
|
89
|
+
if (ret)
|
90
|
+
return ret;
|
91
|
+
|
92
|
+
ret = getConsole(async (stdout, stderr) => {
|
76
93
|
const connect = async () => {
|
77
94
|
const plugins = await getPlugins();
|
78
95
|
const port = await plugins.getRemoteServicePort(peer.selfName, 'console-writer');
|
@@ -93,19 +110,28 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
|
|
93
110
|
};
|
94
111
|
connect();
|
95
112
|
}, undefined, undefined);
|
113
|
+
|
114
|
+
deviceConsoles.set(nativeId, ret);
|
115
|
+
return ret;
|
96
116
|
}
|
97
117
|
|
118
|
+
const mixinConsoles = new Map<string, Map<string, Console>>();
|
119
|
+
|
98
120
|
const getMixinConsole = (mixinId: string, nativeId: ScryptedNativeId) => {
|
99
|
-
|
121
|
+
let nativeIdConsoles = mixinConsoles.get(nativeId);
|
122
|
+
if (!nativeIdConsoles) {
|
123
|
+
nativeIdConsoles = new Map();
|
124
|
+
mixinConsoles.set(nativeId, nativeIdConsoles);
|
125
|
+
}
|
126
|
+
|
127
|
+
let ret = nativeIdConsoles.get(mixinId);
|
128
|
+
if (ret)
|
129
|
+
return ret;
|
130
|
+
|
131
|
+
ret = getConsole(async (stdout, stderr) => {
|
100
132
|
if (!mixinId) {
|
101
133
|
return;
|
102
134
|
}
|
103
|
-
// todo: fix this. a mixin provider can mixin another device to make it a mixin provider itself.
|
104
|
-
// so the mixin id in the mixin table will be incorrect.
|
105
|
-
// there's no easy way to fix this from the remote.
|
106
|
-
// if (!systemManager.getDeviceById(mixinId).mixins.includes(idForNativeId(nativeId))) {
|
107
|
-
// return;
|
108
|
-
// }
|
109
135
|
const reconnect = () => {
|
110
136
|
stdout.removeAllListeners();
|
111
137
|
stderr.removeAllListeners();
|
@@ -147,17 +173,40 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
|
|
147
173
|
}
|
148
174
|
tryConnect();
|
149
175
|
}, getDeviceConsole(nativeId), `[${systemManager.getDeviceById(mixinId)?.name}]`);
|
176
|
+
|
177
|
+
nativeIdConsoles.set(mixinId, ret);
|
178
|
+
return ret;
|
150
179
|
}
|
151
180
|
|
152
|
-
|
153
|
-
|
181
|
+
// process.cpuUsage is for the entire process.
|
182
|
+
// process.memoryUsage is per thread.
|
183
|
+
const allMemoryStats = new Map<NodeThreadWorker, NodeJS.MemoryUsage>();
|
184
|
+
|
185
|
+
peer.getParam('updateStats').then((updateStats: (stats: PluginStats) => void) => {
|
154
186
|
setInterval(() => {
|
155
|
-
const cpuUsage = process.cpuUsage(
|
156
|
-
|
187
|
+
const cpuUsage = process.cpuUsage();
|
188
|
+
allMemoryStats.set(undefined, process.memoryUsage());
|
189
|
+
|
190
|
+
const memoryUsage: NodeJS.MemoryUsage = {
|
191
|
+
rss: 0,
|
192
|
+
heapTotal: 0,
|
193
|
+
heapUsed: 0,
|
194
|
+
external: 0,
|
195
|
+
arrayBuffers: 0,
|
196
|
+
}
|
197
|
+
|
198
|
+
for (const mu of allMemoryStats.values()) {
|
199
|
+
memoryUsage.rss += mu.rss;
|
200
|
+
memoryUsage.heapTotal += mu.heapTotal;
|
201
|
+
memoryUsage.heapUsed += mu.heapUsed;
|
202
|
+
memoryUsage.external += mu.external;
|
203
|
+
memoryUsage.arrayBuffers += mu.arrayBuffers;
|
204
|
+
}
|
205
|
+
|
157
206
|
updateStats({
|
158
207
|
type: 'stats',
|
159
208
|
cpu: cpuUsage,
|
160
|
-
memoryUsage
|
209
|
+
memoryUsage,
|
161
210
|
});
|
162
211
|
}, 10000);
|
163
212
|
});
|
@@ -183,10 +232,6 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
|
|
183
232
|
api = _api;
|
184
233
|
peer.selfName = pluginId;
|
185
234
|
},
|
186
|
-
onPluginReady: async (scrypted, params, plugin) => {
|
187
|
-
replPort = createREPLServer(scrypted, params, plugin);
|
188
|
-
postInstallSourceMapSupport(scrypted);
|
189
|
-
},
|
190
235
|
getPluginConsole,
|
191
236
|
getDeviceConsole,
|
192
237
|
getMixinConsole,
|
@@ -198,7 +243,59 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
|
|
198
243
|
}
|
199
244
|
throw new Error(`unknown service ${name}`);
|
200
245
|
},
|
201
|
-
async onLoadZip(
|
246
|
+
async onLoadZip(scrypted: ScryptedStatic, params: any, packageJson: any, zipData: Buffer | string, zipOptions?: PluginRemoteLoadZipOptions) {
|
247
|
+
let volume: any;
|
248
|
+
let pluginReader: PluginReader;
|
249
|
+
if (zipOptions?.unzippedPath && fs.existsSync(zipOptions?.unzippedPath)) {
|
250
|
+
volume = link(fs, ['', path.join(zipOptions.unzippedPath, 'fs')]);
|
251
|
+
pluginReader = name => {
|
252
|
+
const filename = path.join(zipOptions.unzippedPath, name);
|
253
|
+
if (!fs.existsSync(filename))
|
254
|
+
return;
|
255
|
+
return fs.readFileSync(filename);
|
256
|
+
};
|
257
|
+
}
|
258
|
+
else {
|
259
|
+
const admZip = new AdmZip(zipData);
|
260
|
+
volume = new Volume();
|
261
|
+
for (const entry of admZip.getEntries()) {
|
262
|
+
if (entry.isDirectory)
|
263
|
+
continue;
|
264
|
+
if (!entry.entryName.startsWith('fs/'))
|
265
|
+
continue;
|
266
|
+
const name = entry.entryName.substring('fs/'.length);
|
267
|
+
volume.mkdirpSync(path.dirname(name));
|
268
|
+
const data = entry.getData();
|
269
|
+
volume.writeFileSync(name, data);
|
270
|
+
}
|
271
|
+
|
272
|
+
pluginReader = name => {
|
273
|
+
const entry = admZip.getEntry(name);
|
274
|
+
if (!entry)
|
275
|
+
return;
|
276
|
+
return entry.getData();
|
277
|
+
}
|
278
|
+
}
|
279
|
+
zipData = undefined;
|
280
|
+
|
281
|
+
const pluginConsole = getPluginConsole?.();
|
282
|
+
params.console = pluginConsole;
|
283
|
+
params.require = (name: string) => {
|
284
|
+
if (name === 'fakefs' || (name === 'fs' && !packageJson.scrypted.realfs)) {
|
285
|
+
return volume;
|
286
|
+
}
|
287
|
+
if (name === 'realfs') {
|
288
|
+
return require('fs');
|
289
|
+
}
|
290
|
+
const module = require(name);
|
291
|
+
return module;
|
292
|
+
};
|
293
|
+
const window: any = {};
|
294
|
+
const exports: any = window;
|
295
|
+
window.exports = exports;
|
296
|
+
params.window = window;
|
297
|
+
params.exports = exports;
|
298
|
+
|
202
299
|
const entry = pluginReader('main.nodejs.js.map')
|
203
300
|
const map = entry?.toString();
|
204
301
|
|
@@ -234,10 +331,101 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
|
|
234
331
|
};
|
235
332
|
|
236
333
|
await installOptionalDependencies(getPluginConsole(), packageJson);
|
334
|
+
|
335
|
+
const main = pluginReader('main.nodejs.js');
|
336
|
+
pluginReader = undefined;
|
337
|
+
const script = main.toString();
|
338
|
+
|
339
|
+
|
340
|
+
const forks = new Set<PluginRemote>();
|
341
|
+
|
342
|
+
scrypted.fork = () => {
|
343
|
+
const ntw = new NodeThreadWorker(pluginId, {
|
344
|
+
env: process.env,
|
345
|
+
pluginDebug: undefined,
|
346
|
+
});
|
347
|
+
|
348
|
+
const result = (async () => {
|
349
|
+
const threadPeer = new RpcPeer('main', 'thread', (message, reject) => ntw.send(message, reject));
|
350
|
+
threadPeer.params.updateStats = (stats: PluginStats) => {
|
351
|
+
allMemoryStats.set(ntw, stats.memoryUsage);
|
352
|
+
}
|
353
|
+
ntw.setupRpcPeer(threadPeer);
|
354
|
+
|
355
|
+
class PluginForkAPI extends PluginAPIProxy {
|
356
|
+
[RpcPeer.PROPERTY_PROXY_ONEWAY_METHODS] = (api as any)[RpcPeer.PROPERTY_PROXY_ONEWAY_METHODS];
|
357
|
+
|
358
|
+
setStorage(nativeId: string, storage: { [key: string]: any; }): Promise<void> {
|
359
|
+
const id = deviceManager.nativeIds.get(nativeId).id;
|
360
|
+
(scrypted.pluginRemoteAPI as PluginRemote).setNativeId(nativeId, id, storage);
|
361
|
+
for (const r of forks) {
|
362
|
+
if (r === remote)
|
363
|
+
continue;
|
364
|
+
r.setNativeId(nativeId, id, storage);
|
365
|
+
}
|
366
|
+
return super.setStorage(nativeId, storage);
|
367
|
+
}
|
368
|
+
}
|
369
|
+
const forkApi = new PluginForkAPI(api);
|
370
|
+
|
371
|
+
const remote = await setupPluginRemote(threadPeer, forkApi, pluginId, () => systemManager.getSystemState());
|
372
|
+
forks.add(remote);
|
373
|
+
ntw.worker.on('exit', () => {
|
374
|
+
forkApi.removeListeners();
|
375
|
+
forks.delete(remote);
|
376
|
+
allMemoryStats.delete(ntw);
|
377
|
+
});
|
378
|
+
|
379
|
+
for (const [nativeId, dmd] of deviceManager.nativeIds.entries()) {
|
380
|
+
await remote.setNativeId(nativeId, dmd.id, dmd.storage);
|
381
|
+
}
|
382
|
+
|
383
|
+
const forkOptions = Object.assign({}, zipOptions);
|
384
|
+
forkOptions.fork = true;
|
385
|
+
return remote.loadZip(packageJson, zipData, forkOptions)
|
386
|
+
})();
|
387
|
+
|
388
|
+
result.catch(() => ntw.kill());
|
389
|
+
|
390
|
+
return {
|
391
|
+
worker: ntw.worker,
|
392
|
+
result,
|
393
|
+
}
|
394
|
+
}
|
395
|
+
|
396
|
+
try {
|
397
|
+
peer.evalLocal(script, zipOptions?.filename || '/plugin/main.nodejs.js', params);
|
398
|
+
|
399
|
+
if (zipOptions?.fork) {
|
400
|
+
pluginConsole?.log('plugin forked');
|
401
|
+
const fork = exports.fork;
|
402
|
+
const forked = await fork();
|
403
|
+
forked[RpcPeer.PROPERTY_JSON_DISABLE_SERIALIZATION] = true;
|
404
|
+
return forked;
|
405
|
+
}
|
406
|
+
|
407
|
+
pluginConsole?.log('plugin loaded');
|
408
|
+
let pluginInstance = exports.default;
|
409
|
+
// support exporting a plugin class, plugin main function,
|
410
|
+
// or a plugin instance
|
411
|
+
if (pluginInstance.toString().startsWith('class '))
|
412
|
+
pluginInstance = new pluginInstance();
|
413
|
+
if (typeof pluginInstance === 'function')
|
414
|
+
pluginInstance = await pluginInstance();
|
415
|
+
|
416
|
+
replPort = createREPLServer(scrypted, params, pluginInstance);
|
417
|
+
postInstallSourceMapSupport(scrypted);
|
418
|
+
|
419
|
+
return pluginInstance;
|
420
|
+
}
|
421
|
+
catch (e) {
|
422
|
+
pluginConsole?.error('plugin failed to start', e);
|
423
|
+
throw e;
|
424
|
+
}
|
237
425
|
}
|
238
426
|
}).then(scrypted => {
|
239
427
|
systemManager = scrypted.systemManager;
|
240
|
-
deviceManager = scrypted.deviceManager;
|
428
|
+
deviceManager = scrypted.deviceManager as DeviceManagerImpl;
|
241
429
|
});
|
242
430
|
|
243
431
|
return peer;
|
@@ -1,15 +1,10 @@
|
|
1
|
-
import
|
2
|
-
import { Volume } from 'memfs';
|
3
|
-
import path from 'path';
|
4
|
-
import { ScryptedNativeId, DeviceManager, Logger, Device, DeviceManifest, DeviceState, EndpointManager, SystemDeviceState, ScryptedStatic, SystemManager, MediaManager, ScryptedMimeTypes, ScryptedInterface, ScryptedInterfaceProperty, HttpRequest } from '@scrypted/types'
|
5
|
-
import { PluginAPI, PluginLogger, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
|
6
|
-
import { SystemManagerImpl } from './system';
|
1
|
+
import { Device, DeviceManager, DeviceManifest, DeviceState, EndpointManager, HttpRequest, Logger, MediaManager, ScryptedInterface, ScryptedInterfaceProperty, ScryptedMimeTypes, ScryptedNativeId, ScryptedStatic, SystemDeviceState, SystemManager } from '@scrypted/types';
|
7
2
|
import { RpcPeer, RPCResultError } from '../rpc';
|
8
3
|
import { BufferSerializer } from './buffer-serializer';
|
4
|
+
import { PluginAPI, PluginLogger, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
|
9
5
|
import { createWebSocketClass, WebSocketConnectCallbacks, WebSocketMethods } from './plugin-remote-websocket';
|
10
|
-
import fs from 'fs';
|
11
6
|
import { checkProperty } from './plugin-state-check';
|
12
|
-
|
7
|
+
import { SystemManagerImpl } from './system';
|
13
8
|
|
14
9
|
class DeviceLogger implements Logger {
|
15
10
|
nativeId: ScryptedNativeId;
|
@@ -112,6 +107,10 @@ class DeviceStateProxyHandler implements ProxyHandler<any> {
|
|
112
107
|
get?(target: any, p: PropertyKey, receiver: any) {
|
113
108
|
if (p === 'id')
|
114
109
|
return this.id;
|
110
|
+
if (p === RpcPeer.PROPERTY_PROXY_PROPERTIES)
|
111
|
+
return { id: this.id }
|
112
|
+
if (p === 'setState')
|
113
|
+
return this.setState;
|
115
114
|
return this.deviceManager.systemManager.state[this.id][p as string]?.value;
|
116
115
|
}
|
117
116
|
|
@@ -133,7 +132,7 @@ interface DeviceManagerDevice {
|
|
133
132
|
storage: { [key: string]: any };
|
134
133
|
}
|
135
134
|
|
136
|
-
class DeviceManagerImpl implements DeviceManager {
|
135
|
+
export class DeviceManagerImpl implements DeviceManager {
|
137
136
|
api: PluginAPI;
|
138
137
|
nativeIds = new Map<string, DeviceManagerDevice>();
|
139
138
|
deviceStorage = new Map<string, StorageImpl>();
|
@@ -158,6 +157,11 @@ class DeviceManagerImpl implements DeviceManager {
|
|
158
157
|
return new Proxy(handler, handler);
|
159
158
|
}
|
160
159
|
|
160
|
+
createDeviceState(id: string, setState: (property: string, value: any) => Promise<void>): DeviceState {
|
161
|
+
const handler = new DeviceStateProxyHandler(this, id, setState);
|
162
|
+
return new Proxy(handler, handler);
|
163
|
+
}
|
164
|
+
|
161
165
|
getDeviceStorage(nativeId?: any): StorageImpl {
|
162
166
|
let ret = this.deviceStorage.get(nativeId);
|
163
167
|
if (!ret) {
|
@@ -302,7 +306,7 @@ export async function setupPluginRemote(peer: RpcPeer, api: PluginAPI, pluginId:
|
|
302
306
|
if (!peer.constructorSerializerMap.get(Buffer))
|
303
307
|
peer.addSerializer(Buffer, 'Buffer', new BufferSerializer());
|
304
308
|
const getRemote = await peer.getParam('getRemote');
|
305
|
-
const remote = await getRemote(api, pluginId);
|
309
|
+
const remote = await getRemote(api, pluginId) as PluginRemote;
|
306
310
|
|
307
311
|
await remote.setSystemState(getSystemState());
|
308
312
|
api.listen((id, eventDetails, eventData) => {
|
@@ -342,18 +346,17 @@ export interface WebSocketCustomHandler {
|
|
342
346
|
export type PluginReader = (name: string) => Buffer;
|
343
347
|
|
344
348
|
export interface PluginRemoteAttachOptions {
|
345
|
-
createMediaManager?: (systemManager: SystemManager, deviceManager:
|
349
|
+
createMediaManager?: (systemManager: SystemManager, deviceManager: DeviceManagerImpl) => Promise<MediaManager>;
|
346
350
|
getServicePort?: (name: string, ...args: any[]) => Promise<number>;
|
347
351
|
getDeviceConsole?: (nativeId?: ScryptedNativeId) => Console;
|
348
352
|
getPluginConsole?: () => Console;
|
349
353
|
getMixinConsole?: (id: string, nativeId?: ScryptedNativeId) => Console;
|
350
|
-
onLoadZip?: (
|
354
|
+
onLoadZip?: (scrypted: ScryptedStatic, params: any, packageJson: any, zipData: Buffer | string, zipOptions?: PluginRemoteLoadZipOptions) => Promise<any>;
|
351
355
|
onGetRemote?: (api: PluginAPI, pluginId: string) => Promise<void>;
|
352
|
-
onPluginReady?: (scrypted: ScryptedStatic, params: any, plugin: any) => Promise<void>;
|
353
356
|
}
|
354
357
|
|
355
358
|
export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOptions): Promise<ScryptedStatic> {
|
356
|
-
const { createMediaManager, getServicePort, getDeviceConsole, getMixinConsole
|
359
|
+
const { createMediaManager, getServicePort, getDeviceConsole, getMixinConsole } = options || {};
|
357
360
|
|
358
361
|
if (!peer.constructorSerializerMap.get(Buffer))
|
359
362
|
peer.addSerializer(Buffer, 'Buffer', new BufferSerializer());
|
@@ -367,7 +370,11 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
|
|
367
370
|
const systemManager = new SystemManagerImpl();
|
368
371
|
const deviceManager = new DeviceManagerImpl(systemManager, getDeviceConsole, getMixinConsole);
|
369
372
|
const endpointManager = new EndpointManagerImpl();
|
370
|
-
const
|
373
|
+
const hostMediaManager = await api.getMediaManager();
|
374
|
+
if (!hostMediaManager) {
|
375
|
+
peer.params['createMediaManager'] = async () => createMediaManager(systemManager, deviceManager);
|
376
|
+
}
|
377
|
+
const mediaManager = hostMediaManager || await createMediaManager(systemManager, deviceManager);
|
371
378
|
peer.params['mediaManager'] = mediaManager;
|
372
379
|
const ioSockets: { [id: string]: WebSocketConnectCallbacks } = {};
|
373
380
|
|
@@ -382,6 +389,8 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
|
|
382
389
|
endpointManager,
|
383
390
|
mediaManager,
|
384
391
|
log,
|
392
|
+
pluginHostAPI: api,
|
393
|
+
pluginRemoteAPI: undefined,
|
385
394
|
}
|
386
395
|
|
387
396
|
delete peer.params.getRemote;
|
@@ -403,9 +412,8 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
|
|
403
412
|
'setNativeId',
|
404
413
|
],
|
405
414
|
getServicePort,
|
406
|
-
createDeviceState(id: string, setState: (property: string, value: any) => Promise<void>) {
|
407
|
-
|
408
|
-
return new Proxy(handler, handler);
|
415
|
+
async createDeviceState(id: string, setState: (property: string, value: any) => Promise<void>) {
|
416
|
+
return deviceManager.createDeviceState(id, setState);
|
409
417
|
},
|
410
418
|
|
411
419
|
async ioEvent(id: string, event: string, message?: any) {
|
@@ -472,50 +480,6 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
|
|
472
480
|
},
|
473
481
|
|
474
482
|
async loadZip(packageJson: any, zipData: Buffer | string, zipOptions?: PluginRemoteLoadZipOptions) {
|
475
|
-
const pluginConsole = getPluginConsole?.();
|
476
|
-
|
477
|
-
let volume: any;
|
478
|
-
let pluginReader: PluginReader;
|
479
|
-
if (zipOptions?.unzippedPath && fs.existsSync(zipOptions?.unzippedPath)) {
|
480
|
-
volume = link(fs, ['', path.join(zipOptions.unzippedPath, 'fs')]);
|
481
|
-
pluginReader = name => {
|
482
|
-
const filename = path.join(zipOptions.unzippedPath, name);
|
483
|
-
if (!fs.existsSync(filename))
|
484
|
-
return;
|
485
|
-
return fs.readFileSync(filename);
|
486
|
-
};
|
487
|
-
}
|
488
|
-
else {
|
489
|
-
const admZip = new AdmZip(zipData);
|
490
|
-
volume = new Volume();
|
491
|
-
for (const entry of admZip.getEntries()) {
|
492
|
-
if (entry.isDirectory)
|
493
|
-
continue;
|
494
|
-
if (!entry.entryName.startsWith('fs/'))
|
495
|
-
continue;
|
496
|
-
const name = entry.entryName.substring('fs/'.length);
|
497
|
-
volume.mkdirpSync(path.dirname(name));
|
498
|
-
const data = entry.getData();
|
499
|
-
volume.writeFileSync(name, data);
|
500
|
-
}
|
501
|
-
|
502
|
-
pluginReader = name => {
|
503
|
-
const entry = admZip.getEntry(name);
|
504
|
-
if (!entry)
|
505
|
-
return;
|
506
|
-
return entry.getData();
|
507
|
-
}
|
508
|
-
}
|
509
|
-
zipData = undefined;
|
510
|
-
|
511
|
-
await options?.onLoadZip?.(pluginReader, packageJson);
|
512
|
-
const main = pluginReader('main.nodejs.js');
|
513
|
-
pluginReader = undefined;
|
514
|
-
const script = main.toString();
|
515
|
-
const window: any = {};
|
516
|
-
const exports: any = window;
|
517
|
-
window.exports = exports;
|
518
|
-
|
519
483
|
|
520
484
|
function websocketConnect(url: string, protocols: any, callbacks: WebSocketConnectCallbacks) {
|
521
485
|
if (url.startsWith('io://') || url.startsWith('ws://')) {
|
@@ -535,18 +499,6 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
|
|
535
499
|
|
536
500
|
const params: any = {
|
537
501
|
__filename: undefined,
|
538
|
-
exports,
|
539
|
-
window,
|
540
|
-
require: (name: string) => {
|
541
|
-
if (name === 'fakefs' || (name === 'fs' && !packageJson.scrypted.realfs)) {
|
542
|
-
return volume;
|
543
|
-
}
|
544
|
-
if (name === 'realfs') {
|
545
|
-
return require('fs');
|
546
|
-
}
|
547
|
-
const module = require(name);
|
548
|
-
return module;
|
549
|
-
},
|
550
502
|
deviceManager,
|
551
503
|
systemManager,
|
552
504
|
mediaManager,
|
@@ -555,32 +507,17 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
|
|
555
507
|
localStorage,
|
556
508
|
pluginHostAPI: api,
|
557
509
|
WebSocket: createWebSocketClass(websocketConnect),
|
510
|
+
pluginRuntimeAPI: ret,
|
558
511
|
};
|
559
512
|
|
560
|
-
params.
|
513
|
+
params.pluginRuntimeAPI = ret;
|
561
514
|
|
562
|
-
|
563
|
-
peer.evalLocal(script, zipOptions?.filename || '/plugin/main.nodejs.js', params);
|
564
|
-
pluginConsole?.log('plugin successfully loaded');
|
565
|
-
|
566
|
-
let pluginInstance = exports.default;
|
567
|
-
// support exporting a plugin class, plugin main function,
|
568
|
-
// or a plugin instance
|
569
|
-
if (pluginInstance.toString().startsWith('class '))
|
570
|
-
pluginInstance = new pluginInstance();
|
571
|
-
if (typeof pluginInstance === 'function')
|
572
|
-
pluginInstance = await pluginInstance();
|
573
|
-
|
574
|
-
await options?.onPluginReady?.(ret, params, pluginInstance);
|
575
|
-
return pluginInstance;
|
576
|
-
}
|
577
|
-
catch (e) {
|
578
|
-
pluginConsole?.error('plugin failed to start', e);
|
579
|
-
throw e;
|
580
|
-
}
|
515
|
+
return options.onLoadZip(ret, params, packageJson, zipData, zipOptions);
|
581
516
|
},
|
582
517
|
}
|
583
518
|
|
519
|
+
ret.pluginRemoteAPI = remote;
|
520
|
+
|
584
521
|
return remote;
|
585
522
|
}
|
586
523
|
|