@scrypted/server 0.0.113 → 0.0.118
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @scrypted/server might be problematic. Click here for more details.
- package/.vscode/launch.json +1 -0
- package/dist/http-interfaces.js +11 -4
- package/dist/http-interfaces.js.map +1 -1
- package/dist/level.js.map +1 -1
- package/dist/plugin/media.js +3 -3
- package/dist/plugin/media.js.map +1 -1
- package/dist/plugin/plugin-console.js.map +1 -1
- package/dist/plugin/plugin-device.js +25 -0
- package/dist/plugin/plugin-device.js.map +1 -1
- package/dist/plugin/plugin-host-api.js +2 -2
- package/dist/plugin/plugin-host-api.js.map +1 -1
- package/dist/plugin/plugin-host.js +111 -227
- package/dist/plugin/plugin-host.js.map +1 -1
- package/dist/plugin/plugin-lazy-remote.js.map +1 -1
- package/dist/plugin/plugin-npm-dependencies.js +8 -3
- package/dist/plugin/plugin-npm-dependencies.js.map +1 -1
- package/dist/plugin/plugin-remote-worker.js +253 -0
- package/dist/plugin/plugin-remote-worker.js.map +1 -0
- package/dist/plugin/plugin-remote.js +39 -15
- package/dist/plugin/plugin-remote.js.map +1 -1
- package/dist/plugin/plugin-repl.js +3 -1
- package/dist/plugin/plugin-repl.js.map +1 -1
- package/dist/plugin/system.js +11 -6
- package/dist/plugin/system.js.map +1 -1
- package/dist/rpc.js +11 -1
- package/dist/rpc.js.map +1 -1
- package/dist/runtime.js +19 -4
- package/dist/runtime.js.map +1 -1
- package/dist/scrypted-main.js +3 -437
- package/dist/scrypted-main.js.map +1 -1
- package/dist/scrypted-plugin-main.js +8 -0
- package/dist/scrypted-plugin-main.js.map +1 -0
- package/dist/scrypted-server-main.js +448 -0
- package/dist/scrypted-server-main.js.map +1 -0
- package/package.json +5 -4
- package/python/media.py +1 -12
- package/python/plugin-remote.py +15 -7
- package/python/rpc.py +1 -1
- package/src/http-interfaces.ts +12 -5
- package/src/level.ts +0 -2
- package/src/plugin/media.ts +3 -3
- package/src/plugin/plugin-api.ts +9 -1
- package/src/plugin/plugin-console.ts +0 -1
- package/src/plugin/plugin-device.ts +26 -2
- package/src/plugin/plugin-host-api.ts +2 -2
- package/src/plugin/plugin-host.ts +122 -253
- package/src/plugin/plugin-http.ts +2 -2
- package/src/plugin/plugin-lazy-remote.ts +1 -1
- package/src/plugin/plugin-npm-dependencies.ts +7 -2
- package/src/plugin/plugin-remote-worker.ts +272 -0
- package/src/plugin/plugin-remote.ts +46 -17
- package/src/plugin/plugin-repl.ts +4 -2
- package/src/plugin/system.ts +15 -13
- package/src/rpc.ts +18 -3
- package/src/runtime.ts +19 -4
- package/src/scrypted-main.ts +3 -508
- package/src/scrypted-plugin-main.ts +6 -0
- package/src/scrypted-server-main.ts +516 -0
package/src/plugin/media.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { once } from 'events';
|
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import tmp from 'tmp';
|
|
9
9
|
import os from 'os';
|
|
10
|
-
import
|
|
10
|
+
import { getInstalledFfmpeg } from '@scrypted/ffmpeg'
|
|
11
11
|
import { ffmpegLogInitialOutput } from "../media-helpers";
|
|
12
12
|
|
|
13
13
|
function addBuiltins(console: Console, mediaManager: MediaManager) {
|
|
@@ -94,7 +94,7 @@ function addBuiltins(console: Console, mediaManager: MediaManager) {
|
|
|
94
94
|
})
|
|
95
95
|
const to = setTimeout(() => {
|
|
96
96
|
console.log('ffmpeg stream to image convesion timed out.');
|
|
97
|
-
cp.kill();
|
|
97
|
+
cp.kill('SIGKILL');
|
|
98
98
|
}, 10000);
|
|
99
99
|
await once(cp, 'exit');
|
|
100
100
|
clearTimeout(to);
|
|
@@ -133,7 +133,7 @@ export abstract class MediaManagerBase implements MediaManager {
|
|
|
133
133
|
return f;
|
|
134
134
|
|
|
135
135
|
const defaultPath = os.platform() === 'win32' ? 'ffmpeg.exe' : 'ffmpeg';
|
|
136
|
-
return
|
|
136
|
+
return getInstalledFfmpeg() || defaultPath;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
getConverters(): BufferConverter[] {
|
package/src/plugin/plugin-api.ts
CHANGED
|
@@ -132,11 +132,19 @@ export class PluginAPIProxy extends PluginAPIManagedListeners implements PluginA
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
export interface PluginRemoteLoadZipOptions {
|
|
135
|
+
/**
|
|
136
|
+
* The filename to load the script as. Use for debugger source mapping.
|
|
137
|
+
*/
|
|
135
138
|
filename?: string;
|
|
139
|
+
/**
|
|
140
|
+
* The path that the zip is currently unzipped at on the server. May not
|
|
141
|
+
* exist on the "remote", if it is not the same machine.
|
|
142
|
+
*/
|
|
143
|
+
unzippedPath?: string;
|
|
136
144
|
}
|
|
137
145
|
|
|
138
146
|
export interface PluginRemote {
|
|
139
|
-
loadZip(packageJson: any, zipData: Buffer, options?: PluginRemoteLoadZipOptions): Promise<any>;
|
|
147
|
+
loadZip(packageJson: any, zipData: Buffer|string, options?: PluginRemoteLoadZipOptions): Promise<any>;
|
|
140
148
|
setSystemState(state: {[id: string]: {[property: string]: SystemDeviceState}}): Promise<void>;
|
|
141
149
|
setNativeId(nativeId: ScryptedNativeId, id: string, storage: {[key: string]: any}): Promise<void>;
|
|
142
150
|
updateDeviceState(id: string, state: {[property: string]: SystemDeviceState}): Promise<void>;
|
|
@@ -2,12 +2,14 @@ import { DeviceProvider, EventDetails, EventListenerOptions, EventListenerRegist
|
|
|
2
2
|
import { ScryptedRuntime } from "../runtime";
|
|
3
3
|
import { PluginDevice } from "../db-types";
|
|
4
4
|
import { MixinProvider } from "@scrypted/sdk/types";
|
|
5
|
-
import { handleFunctionInvocations } from "../rpc";
|
|
5
|
+
import { handleFunctionInvocations, PrimitiveProxyHandler } from "../rpc";
|
|
6
6
|
import { getState } from "../state";
|
|
7
7
|
import { getDisplayType } from "../infer-defaults";
|
|
8
8
|
import { allInterfaceProperties, isValidInterfaceMethod, methodInterfaces } from "./descriptor";
|
|
9
9
|
import { PluginError } from "./plugin-error";
|
|
10
10
|
import { sleep } from "../sleep";
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import fs from 'fs';
|
|
11
13
|
|
|
12
14
|
interface MixinTable {
|
|
13
15
|
mixinProviderId: string;
|
|
@@ -25,7 +27,7 @@ interface MixinTableEntry {
|
|
|
25
27
|
export const RefreshSymbol = Symbol('ScryptedDeviceRefresh');
|
|
26
28
|
export const QueryInterfaceSymbol = Symbol("ScryptedPluginDeviceQueryInterface");
|
|
27
29
|
|
|
28
|
-
export class PluginDeviceProxyHandler implements
|
|
30
|
+
export class PluginDeviceProxyHandler implements PrimitiveProxyHandler<any>, ScryptedDevice {
|
|
29
31
|
scrypted: ScryptedRuntime;
|
|
30
32
|
id: string;
|
|
31
33
|
mixinTable: MixinTable[];
|
|
@@ -36,6 +38,10 @@ export class PluginDeviceProxyHandler implements ProxyHandler<any>, ScryptedDevi
|
|
|
36
38
|
this.id = id;
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
toPrimitive() {
|
|
42
|
+
return `PluginDevice-${this.id}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
39
45
|
invalidateEntry(mixinEntry: MixinTable) {
|
|
40
46
|
if (!mixinEntry?.mixinProviderId)
|
|
41
47
|
return;
|
|
@@ -373,6 +379,24 @@ export class PluginDeviceProxyHandler implements ProxyHandler<any>, ScryptedDevi
|
|
|
373
379
|
throw new PluginError(`${iface} not implemented`)
|
|
374
380
|
}
|
|
375
381
|
|
|
382
|
+
if (method === 'getReadmeMarkdown') {
|
|
383
|
+
const pluginDevice = this.scrypted.findPluginDeviceById(this.id);
|
|
384
|
+
if (pluginDevice && !pluginDevice.nativeId) {
|
|
385
|
+
const plugin = this.scrypted.plugins[pluginDevice.pluginId];
|
|
386
|
+
if (!plugin.packageJson.scrypted.interfaces.includes(ScryptedInterface.Readme)) {
|
|
387
|
+
const readmePath = path.join(plugin.unzippedPath, 'README.md');
|
|
388
|
+
if (fs.existsSync(readmePath)) {
|
|
389
|
+
try {
|
|
390
|
+
return fs.readFileSync(readmePath).toString();
|
|
391
|
+
}
|
|
392
|
+
catch (e) {
|
|
393
|
+
return "# Error loading Readme:\n\n" + e;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
376
400
|
if (!isValidInterfaceMethod(pluginDevice.state.interfaces.value, method))
|
|
377
401
|
throw new PluginError(`device ${this.id} does not support method ${method}`);
|
|
378
402
|
|
|
@@ -35,9 +35,9 @@ export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAP
|
|
|
35
35
|
this.scrypted.runPlugin(plugin);
|
|
36
36
|
}, 15000);
|
|
37
37
|
|
|
38
|
-
constructor(public scrypted: ScryptedRuntime,
|
|
38
|
+
constructor(public scrypted: ScryptedRuntime, pluginId: string, public pluginHost: PluginHost, public mediaManager: MediaManager) {
|
|
39
39
|
super();
|
|
40
|
-
this.pluginId =
|
|
40
|
+
this.pluginId = pluginId;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// do we care about mixin validation here?
|
|
@@ -1,39 +1,38 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RpcPeer } from '../rpc';
|
|
2
2
|
import AdmZip from 'adm-zip';
|
|
3
|
-
import {
|
|
3
|
+
import { Device, EngineIOHandler } from '@scrypted/sdk/types'
|
|
4
4
|
import { ScryptedRuntime } from '../runtime';
|
|
5
5
|
import { Plugin } from '../db-types';
|
|
6
6
|
import io, { Socket } from 'engine.io';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { setupPluginRemote } from './plugin-remote';
|
|
8
|
+
import { PluginAPIProxy, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
|
|
9
9
|
import { Logger } from '../logger';
|
|
10
|
-
import { MediaManagerHostImpl
|
|
10
|
+
import { MediaManagerHostImpl } from './media';
|
|
11
11
|
import WebSocket from 'ws';
|
|
12
|
-
import { PassThrough } from 'stream';
|
|
13
|
-
import { Console } from 'console'
|
|
14
12
|
import { sleep } from '../sleep';
|
|
15
13
|
import { PluginHostAPI } from './plugin-host-api';
|
|
16
14
|
import path from 'path';
|
|
17
|
-
import { install as installSourceMapSupport } from 'source-map-support';
|
|
18
|
-
import net from 'net'
|
|
19
15
|
import child_process from 'child_process';
|
|
20
16
|
import { PluginDebug } from './plugin-debug';
|
|
21
17
|
import readline from 'readline';
|
|
22
18
|
import { Readable, Writable } from 'stream';
|
|
23
|
-
import { ensurePluginVolume } from './plugin-volume';
|
|
24
|
-
import {
|
|
19
|
+
import { ensurePluginVolume, getScryptedVolume } from './plugin-volume';
|
|
20
|
+
import { getPluginNodePath } from './plugin-npm-dependencies';
|
|
25
21
|
import { ConsoleServer, createConsoleServer } from './plugin-console';
|
|
26
|
-
import { createREPLServer } from './plugin-repl';
|
|
27
22
|
import { LazyRemote } from './plugin-lazy-remote';
|
|
23
|
+
import crypto from 'crypto';
|
|
24
|
+
import fs from 'fs';
|
|
25
|
+
import mkdirp from 'mkdirp';
|
|
26
|
+
import rimraf from 'rimraf';
|
|
28
27
|
|
|
29
28
|
export class PluginHost {
|
|
29
|
+
static sharedWorker: child_process.ChildProcess;
|
|
30
30
|
worker: child_process.ChildProcess;
|
|
31
31
|
peer: RpcPeer;
|
|
32
32
|
pluginId: string;
|
|
33
33
|
module: Promise<any>;
|
|
34
34
|
scrypted: ScryptedRuntime;
|
|
35
35
|
remote: PluginRemote;
|
|
36
|
-
zip: AdmZip;
|
|
37
36
|
io = io(undefined, {
|
|
38
37
|
pingTimeout: 120000,
|
|
39
38
|
});
|
|
@@ -47,11 +46,12 @@ export class PluginHost {
|
|
|
47
46
|
};
|
|
48
47
|
killed = false;
|
|
49
48
|
consoleServer: Promise<ConsoleServer>;
|
|
49
|
+
unzippedPath: string;
|
|
50
50
|
|
|
51
51
|
kill() {
|
|
52
52
|
this.killed = true;
|
|
53
53
|
this.api.removeListeners();
|
|
54
|
-
this.worker.kill();
|
|
54
|
+
this.worker.kill('SIGKILL');
|
|
55
55
|
this.io.close();
|
|
56
56
|
for (const s of Object.values(this.ws)) {
|
|
57
57
|
s.close();
|
|
@@ -86,15 +86,19 @@ export class PluginHost {
|
|
|
86
86
|
this.pluginId = plugin._id;
|
|
87
87
|
this.pluginName = plugin.packageJson?.name;
|
|
88
88
|
this.packageJson = plugin.packageJson;
|
|
89
|
-
|
|
89
|
+
let zipBuffer = Buffer.from(plugin.zip, 'base64');
|
|
90
|
+
// allow garbage collection of the base 64 contents
|
|
91
|
+
plugin = undefined;
|
|
90
92
|
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
+
const logger = scrypted.getDeviceLogger(scrypted.findPluginDevice(this.pluginId));
|
|
94
|
+
|
|
95
|
+
const volume = getScryptedVolume();
|
|
96
|
+
const pluginVolume = ensurePluginVolume(this.pluginId);
|
|
93
97
|
|
|
94
98
|
this.startPluginHost(logger, {
|
|
95
|
-
NODE_PATH: path.join(
|
|
96
|
-
SCRYPTED_PLUGIN_VOLUME:
|
|
97
|
-
},
|
|
99
|
+
NODE_PATH: path.join(getPluginNodePath(this.pluginId), 'node_modules'),
|
|
100
|
+
SCRYPTED_PLUGIN_VOLUME: pluginVolume,
|
|
101
|
+
}, this.packageJson.scrypted.runtime);
|
|
98
102
|
|
|
99
103
|
this.io.on('connection', async (socket) => {
|
|
100
104
|
try {
|
|
@@ -138,10 +142,26 @@ export class PluginHost {
|
|
|
138
142
|
? new MediaManagerHostImpl(scrypted.stateManager.getSystemState(), id => scrypted.getDevice(id), console)
|
|
139
143
|
: undefined;
|
|
140
144
|
|
|
141
|
-
this.api = new PluginHostAPI(scrypted,
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
+
this.api = new PluginHostAPI(scrypted, this.pluginId, this, mediaManager);
|
|
146
|
+
|
|
147
|
+
const zipDir = path.join(pluginVolume, 'zip');
|
|
148
|
+
const extractVersion = "1-";
|
|
149
|
+
const hash = extractVersion + crypto.createHash('md5').update(zipBuffer).digest().toString('hex');
|
|
150
|
+
const zipFilename = `${hash}.zip`;
|
|
151
|
+
const zipFile = path.join(zipDir, zipFilename);
|
|
152
|
+
this.unzippedPath = path.join(zipDir, 'unzipped')
|
|
153
|
+
{
|
|
154
|
+
const zipDirTmp = zipDir + '.tmp';
|
|
155
|
+
if (!fs.existsSync(zipFile)) {
|
|
156
|
+
rimraf.sync(zipDirTmp);
|
|
157
|
+
rimraf.sync(zipDir);
|
|
158
|
+
mkdirp.sync(zipDirTmp);
|
|
159
|
+
fs.writeFileSync(path.join(zipDirTmp, zipFilename), zipBuffer);
|
|
160
|
+
const admZip = new AdmZip(zipBuffer);
|
|
161
|
+
admZip.extractAllTo(path.join(zipDirTmp, 'unzipped'), true);
|
|
162
|
+
fs.renameSync(zipDirTmp, zipDir);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
145
165
|
|
|
146
166
|
logger.log('i', `loading ${this.pluginName}`);
|
|
147
167
|
logger.log('i', 'pid ' + this.worker?.pid);
|
|
@@ -170,18 +190,25 @@ export class PluginHost {
|
|
|
170
190
|
|
|
171
191
|
const fail = 'Plugin failed to load. Console for more information.';
|
|
172
192
|
try {
|
|
193
|
+
const isPython = runtime === 'python';
|
|
173
194
|
const loadZipOptions: PluginRemoteLoadZipOptions = {
|
|
174
195
|
// if debugging, use a normalized path for sourcemap resolution, otherwise
|
|
175
196
|
// prefix with module path.
|
|
176
|
-
filename:
|
|
197
|
+
filename: isPython
|
|
177
198
|
? pluginDebug
|
|
178
199
|
? `${volume}/plugin.zip`
|
|
179
|
-
:
|
|
200
|
+
: zipFile
|
|
180
201
|
: pluginDebug
|
|
181
202
|
? '/plugin/main.nodejs.js'
|
|
182
203
|
: `/${this.pluginId}/main.nodejs.js`,
|
|
204
|
+
unzippedPath: this.unzippedPath,
|
|
183
205
|
};
|
|
184
|
-
|
|
206
|
+
// original implementation sent the zipBuffer, sending the zipFile name now.
|
|
207
|
+
// can switch back for non-local plugins.
|
|
208
|
+
const modulePromise = remote.loadZip(this.packageJson, zipFile, loadZipOptions);
|
|
209
|
+
// allow garbage collection of the zip buffer
|
|
210
|
+
zipBuffer = undefined;
|
|
211
|
+
const module = await modulePromise;
|
|
185
212
|
logger.log('i', `loaded ${this.pluginName}`);
|
|
186
213
|
logger.clearAlert(fail)
|
|
187
214
|
return { module, remote };
|
|
@@ -260,27 +287,73 @@ export class PluginHost {
|
|
|
260
287
|
execArgv.push(`--inspect=0.0.0.0:${this.pluginDebug.inspectPort}`);
|
|
261
288
|
}
|
|
262
289
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
reject(e);
|
|
290
|
+
const useSharedWorker = process.env.SCRYPTED_SHARED_WORKER &&
|
|
291
|
+
this.packageJson.scrypted.sharedWorker !== false &&
|
|
292
|
+
this.packageJson.scrypted.realfs !== true &&
|
|
293
|
+
Object.keys(this.packageJson.optionalDependencies || {}).length === 0;
|
|
294
|
+
if (useSharedWorker) {
|
|
295
|
+
if (!PluginHost.sharedWorker) {
|
|
296
|
+
PluginHost.sharedWorker = child_process.fork(require.main.filename, ['child', '@scrypted/shared'], {
|
|
297
|
+
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
|
|
298
|
+
env: Object.assign({}, process.env, env),
|
|
299
|
+
serialization: 'advanced',
|
|
300
|
+
execArgv,
|
|
275
301
|
});
|
|
302
|
+
PluginHost.sharedWorker.setMaxListeners(100);
|
|
303
|
+
PluginHost.sharedWorker.on('close', () => PluginHost.sharedWorker = undefined);
|
|
304
|
+
PluginHost.sharedWorker.on('error', () => PluginHost.sharedWorker = undefined);
|
|
305
|
+
PluginHost.sharedWorker.on('exit', () => PluginHost.sharedWorker = undefined);
|
|
306
|
+
PluginHost.sharedWorker.on('disconnect', () => PluginHost.sharedWorker = undefined);
|
|
276
307
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
308
|
+
PluginHost.sharedWorker.send({
|
|
309
|
+
type: 'start',
|
|
310
|
+
pluginId: this.pluginId,
|
|
311
|
+
});
|
|
312
|
+
this.worker = PluginHost.sharedWorker;
|
|
313
|
+
this.worker.on('message', (message: any) => {
|
|
314
|
+
if (message.pluginId === this.pluginId)
|
|
315
|
+
this.peer.handleMessage(message.message)
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
this.peer = new RpcPeer('host', this.pluginId, (message, reject) => {
|
|
319
|
+
if (connected) {
|
|
320
|
+
this.worker.send({
|
|
321
|
+
type: 'message',
|
|
322
|
+
pluginId: this.pluginId,
|
|
323
|
+
message: message,
|
|
324
|
+
}, undefined, e => {
|
|
325
|
+
if (e && reject)
|
|
326
|
+
reject(e);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
else if (reject) {
|
|
330
|
+
reject(new Error('peer disconnected'));
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
this.worker = child_process.fork(require.main.filename, ['child', this.pluginId], {
|
|
336
|
+
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
|
|
337
|
+
env: Object.assign({}, process.env, env),
|
|
338
|
+
serialization: 'advanced',
|
|
339
|
+
execArgv,
|
|
340
|
+
});
|
|
341
|
+
this.worker.on('message', message => this.peer.handleMessage(message as any));
|
|
342
|
+
|
|
343
|
+
this.peer = new RpcPeer('host', this.pluginId, (message, reject) => {
|
|
344
|
+
if (connected) {
|
|
345
|
+
this.worker.send(message, undefined, e => {
|
|
346
|
+
if (e && reject)
|
|
347
|
+
reject(e);
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
else if (reject) {
|
|
351
|
+
reject(new Error('peer disconnected'));
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
}
|
|
282
355
|
|
|
283
|
-
this.
|
|
356
|
+
this.peer.transportSafeArgumentTypes.add(Buffer.name);
|
|
284
357
|
}
|
|
285
358
|
|
|
286
359
|
this.worker.stdout.on('data', data => console.log(data.toString()));
|
|
@@ -292,6 +365,10 @@ export class PluginHost {
|
|
|
292
365
|
pluginConsole.log('starting plugin', this.pluginId, this.packageJson.version);
|
|
293
366
|
});
|
|
294
367
|
|
|
368
|
+
this.worker.on('close', () => {
|
|
369
|
+
connected = false;
|
|
370
|
+
logger.log('e', `${this.pluginName} close`);
|
|
371
|
+
});
|
|
295
372
|
this.worker.on('disconnect', () => {
|
|
296
373
|
connected = false;
|
|
297
374
|
logger.log('e', `${this.pluginName} disconnected`);
|
|
@@ -333,211 +410,3 @@ export class PluginHost {
|
|
|
333
410
|
return setupPluginRemote(rpcPeer, api, null, () => this.scrypted.stateManager.getSystemState());
|
|
334
411
|
}
|
|
335
412
|
}
|
|
336
|
-
|
|
337
|
-
export function startPluginRemote() {
|
|
338
|
-
const peer = new RpcPeer('unknown', 'host', (message, reject) => process.send(message, undefined, {
|
|
339
|
-
swallowErrors: !reject,
|
|
340
|
-
}, e => {
|
|
341
|
-
if (e)
|
|
342
|
-
reject?.(e);
|
|
343
|
-
}));
|
|
344
|
-
peer.transportSafeArgumentTypes.add(Buffer.name);
|
|
345
|
-
process.on('message', message => peer.handleMessage(message as RpcMessage));
|
|
346
|
-
|
|
347
|
-
let systemManager: SystemManager;
|
|
348
|
-
let deviceManager: DeviceManager;
|
|
349
|
-
let api: PluginAPI;
|
|
350
|
-
let pluginId: string;
|
|
351
|
-
|
|
352
|
-
const getConsole = (hook: (stdout: PassThrough, stderr: PassThrough) => Promise<void>,
|
|
353
|
-
also?: Console, alsoPrefix?: string) => {
|
|
354
|
-
|
|
355
|
-
const stdout = new PassThrough();
|
|
356
|
-
const stderr = new PassThrough();
|
|
357
|
-
|
|
358
|
-
hook(stdout, stderr);
|
|
359
|
-
|
|
360
|
-
const ret = new Console(stdout, stderr);
|
|
361
|
-
|
|
362
|
-
const methods = [
|
|
363
|
-
'log', 'warn',
|
|
364
|
-
'dir', 'time',
|
|
365
|
-
'timeEnd', 'timeLog',
|
|
366
|
-
'trace', 'assert',
|
|
367
|
-
'clear', 'count',
|
|
368
|
-
'countReset', 'group',
|
|
369
|
-
'groupEnd', 'table',
|
|
370
|
-
'debug', 'info',
|
|
371
|
-
'dirxml', 'error',
|
|
372
|
-
'groupCollapsed',
|
|
373
|
-
];
|
|
374
|
-
|
|
375
|
-
const printers = ['log', 'info', 'debug', 'trace', 'warn', 'error'];
|
|
376
|
-
for (const m of methods) {
|
|
377
|
-
const old = (ret as any)[m].bind(ret);
|
|
378
|
-
(ret as any)[m] = (...args: any[]) => {
|
|
379
|
-
// prefer the mixin version for local/remote console dump.
|
|
380
|
-
if (also && alsoPrefix && printers.includes(m)) {
|
|
381
|
-
(also as any)[m](alsoPrefix, ...args);
|
|
382
|
-
}
|
|
383
|
-
else {
|
|
384
|
-
(console as any)[m](...args);
|
|
385
|
-
}
|
|
386
|
-
// call through to old method to ensure it gets written
|
|
387
|
-
// to log buffer.
|
|
388
|
-
old(...args);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
return ret;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const getDeviceConsole = (nativeId?: ScryptedNativeId) => {
|
|
396
|
-
// the the plugin console is simply the default console
|
|
397
|
-
// and gets read from stderr/stdout.
|
|
398
|
-
if (!nativeId)
|
|
399
|
-
return console;
|
|
400
|
-
|
|
401
|
-
return getConsole(async (stdout, stderr) => {
|
|
402
|
-
const plugins = await api.getComponent('plugins');
|
|
403
|
-
const connect = async () => {
|
|
404
|
-
const port = await plugins.getRemoteServicePort(peer.selfName, 'console-writer');
|
|
405
|
-
const socket = net.connect(port);
|
|
406
|
-
socket.write(nativeId + '\n');
|
|
407
|
-
const writer = (data: Buffer) => {
|
|
408
|
-
socket.write(data);
|
|
409
|
-
};
|
|
410
|
-
stdout.on('data', writer);
|
|
411
|
-
stderr.on('data', writer);
|
|
412
|
-
socket.on('error', () => {
|
|
413
|
-
stdout.removeAllListeners();
|
|
414
|
-
stderr.removeAllListeners();
|
|
415
|
-
stdout.pause();
|
|
416
|
-
stderr.pause();
|
|
417
|
-
setTimeout(connect, 10000);
|
|
418
|
-
});
|
|
419
|
-
};
|
|
420
|
-
connect();
|
|
421
|
-
}, undefined, undefined);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const getMixinConsole = (mixinId: string, nativeId: ScryptedNativeId) => {
|
|
425
|
-
return getConsole(async (stdout, stderr) => {
|
|
426
|
-
if (!mixinId) {
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
// todo: fix this. a mixin provider can mixin another device to make it a mixin provider itself.
|
|
430
|
-
// so the mixin id in the mixin table will be incorrect.
|
|
431
|
-
// there's no easy way to fix this from the remote.
|
|
432
|
-
// if (!systemManager.getDeviceById(mixinId).mixins.includes(idForNativeId(nativeId))) {
|
|
433
|
-
// return;
|
|
434
|
-
// }
|
|
435
|
-
const plugins = await systemManager.getComponent('plugins');
|
|
436
|
-
const connect = async () => {
|
|
437
|
-
const ds = deviceManager.getDeviceState(nativeId);
|
|
438
|
-
if (!ds) {
|
|
439
|
-
// deleted?
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
const { pluginId, nativeId: mixinNativeId } = await plugins.getDeviceInfo(mixinId);
|
|
443
|
-
const port = await plugins.getRemoteServicePort(pluginId, 'console-writer');
|
|
444
|
-
const socket = net.connect(port);
|
|
445
|
-
socket.write(mixinNativeId + '\n');
|
|
446
|
-
const writer = (data: Buffer) => {
|
|
447
|
-
let str = data.toString().trim();
|
|
448
|
-
str = str.replaceAll('\n', `\n[${ds.name}]: `);
|
|
449
|
-
str = `[${ds.name}]: ` + str + '\n';
|
|
450
|
-
socket.write(str);
|
|
451
|
-
};
|
|
452
|
-
stdout.on('data', writer);
|
|
453
|
-
stderr.on('data', writer);
|
|
454
|
-
socket.on('error', () => {
|
|
455
|
-
stdout.removeAllListeners();
|
|
456
|
-
stderr.removeAllListeners();
|
|
457
|
-
stdout.pause();
|
|
458
|
-
stderr.pause();
|
|
459
|
-
setTimeout(connect, 10000);
|
|
460
|
-
});
|
|
461
|
-
};
|
|
462
|
-
connect();
|
|
463
|
-
}, getDeviceConsole(nativeId), `[${systemManager.getDeviceById(mixinId)?.name}]`);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
let lastCpuUsage: NodeJS.CpuUsage;
|
|
467
|
-
setInterval(() => {
|
|
468
|
-
const cpuUsage = process.cpuUsage(lastCpuUsage);
|
|
469
|
-
lastCpuUsage = cpuUsage;
|
|
470
|
-
peer.sendOob({
|
|
471
|
-
type: 'stats',
|
|
472
|
-
cpu: cpuUsage,
|
|
473
|
-
memoryUsage: process.memoryUsage(),
|
|
474
|
-
});
|
|
475
|
-
global?.gc();
|
|
476
|
-
}, 10000);
|
|
477
|
-
|
|
478
|
-
let replPort: Promise<number>;
|
|
479
|
-
|
|
480
|
-
let _pluginConsole: Console;
|
|
481
|
-
const getPluginConsole = () => {
|
|
482
|
-
if (!_pluginConsole)
|
|
483
|
-
_pluginConsole = getDeviceConsole(undefined);
|
|
484
|
-
return _pluginConsole;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
attachPluginRemote(peer, {
|
|
488
|
-
createMediaManager: async (sm) => {
|
|
489
|
-
systemManager = sm;
|
|
490
|
-
return new MediaManagerImpl(systemManager, getPluginConsole());
|
|
491
|
-
},
|
|
492
|
-
onGetRemote: async (_api, _pluginId) => {
|
|
493
|
-
api = _api;
|
|
494
|
-
pluginId = _pluginId;
|
|
495
|
-
peer.selfName = pluginId;
|
|
496
|
-
},
|
|
497
|
-
onPluginReady: async (scrypted, params, plugin) => {
|
|
498
|
-
replPort = createREPLServer(scrypted, params, plugin);
|
|
499
|
-
},
|
|
500
|
-
getPluginConsole,
|
|
501
|
-
getDeviceConsole,
|
|
502
|
-
getMixinConsole,
|
|
503
|
-
async getServicePort(name, ...args: any[]) {
|
|
504
|
-
if (name === 'repl') {
|
|
505
|
-
if (!replPort)
|
|
506
|
-
throw new Error('REPL unavailable: Plugin not loaded.')
|
|
507
|
-
return replPort;
|
|
508
|
-
}
|
|
509
|
-
throw new Error(`unknown service ${name}`);
|
|
510
|
-
},
|
|
511
|
-
async onLoadZip(zip: AdmZip, packageJson: any) {
|
|
512
|
-
installSourceMapSupport({
|
|
513
|
-
environment: 'node',
|
|
514
|
-
retrieveSourceMap(source) {
|
|
515
|
-
if (source === '/plugin/main.nodejs.js' || source === `/${pluginId}/main.nodejs.js`) {
|
|
516
|
-
const entry = zip.getEntry('main.nodejs.js.map')
|
|
517
|
-
const map = entry?.getData().toString();
|
|
518
|
-
if (!map)
|
|
519
|
-
return null;
|
|
520
|
-
return {
|
|
521
|
-
url: '/plugin/main.nodejs.js',
|
|
522
|
-
map,
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
return null;
|
|
526
|
-
}
|
|
527
|
-
});
|
|
528
|
-
await installOptionalDependencies(getPluginConsole(), packageJson);
|
|
529
|
-
}
|
|
530
|
-
}).then(scrypted => {
|
|
531
|
-
systemManager = scrypted.systemManager;
|
|
532
|
-
deviceManager = scrypted.deviceManager;
|
|
533
|
-
|
|
534
|
-
process.on('uncaughtException', e => {
|
|
535
|
-
getPluginConsole().error('uncaughtException', e);
|
|
536
|
-
scrypted.log.e('uncaughtException ' + e?.toString());
|
|
537
|
-
});
|
|
538
|
-
process.on('unhandledRejection', e => {
|
|
539
|
-
getPluginConsole().error('unhandledRejection', e);
|
|
540
|
-
scrypted.log.e('unhandledRejection ' + e?.toString());
|
|
541
|
-
});
|
|
542
|
-
})
|
|
543
|
-
}
|
|
@@ -28,8 +28,8 @@ export abstract class PluginHttp<T> {
|
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
abstract handleEngineIOEndpoint(req: Request, res: ServerResponse, endpointRequest: HttpRequest, pluginData: T):
|
|
32
|
-
abstract handleRequestEndpoint(req: Request, res: Response, endpointRequest: HttpRequest, pluginData: T):
|
|
31
|
+
abstract handleEngineIOEndpoint(req: Request, res: ServerResponse, endpointRequest: HttpRequest, pluginData: T): void;
|
|
32
|
+
abstract handleRequestEndpoint(req: Request, res: Response, endpointRequest: HttpRequest, pluginData: T): void;
|
|
33
33
|
abstract getEndpointPluginData(endpoint: string, isUpgrade: boolean, isEngineIOEndpoint: boolean): Promise<T>;
|
|
34
34
|
abstract handleWebSocket(endpoint: string, httpRequest: HttpRequest, ws: WebSocket, pluginData: T): Promise<void>;
|
|
35
35
|
|
|
@@ -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);
|
|
@@ -6,9 +6,14 @@ import { once } from 'events';
|
|
|
6
6
|
import process from 'process';
|
|
7
7
|
import mkdirp from "mkdirp";
|
|
8
8
|
|
|
9
|
-
export
|
|
10
|
-
const pluginVolume = ensurePluginVolume(
|
|
9
|
+
export function getPluginNodePath(name: string) {
|
|
10
|
+
const pluginVolume = ensurePluginVolume(name);
|
|
11
11
|
const nodePrefix = path.join(pluginVolume, `${process.platform}-${process.arch}`);
|
|
12
|
+
return nodePrefix;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function installOptionalDependencies(console: Console, packageJson: any) {
|
|
16
|
+
const nodePrefix = getPluginNodePath(packageJson.name);
|
|
12
17
|
const packageJsonPath = path.join(nodePrefix, 'package.json');
|
|
13
18
|
const currentInstalledPackageJsonPath = path.join(nodePrefix, 'package.installed.json');
|
|
14
19
|
|