@scrypted/server 0.0.116 → 0.0.121
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 +4 -4
- package/dist/plugin/media.js.map +1 -1
- package/dist/plugin/plugin-console.js.map +1 -1
- package/dist/plugin/plugin-device.js +13 -3
- 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 +110 -235
- package/dist/plugin/plugin-host.js.map +1 -1
- package/dist/plugin/plugin-http.js +1 -0
- package/dist/plugin/plugin-http.js.map +1 -1
- package/dist/plugin/plugin-lazy-remote.js.map +1 -1
- package/dist/plugin/plugin-remote-worker.js +252 -0
- package/dist/plugin/plugin-remote-worker.js.map +1 -0
- package/dist/plugin/plugin-remote.js +38 -14
- package/dist/plugin/plugin-remote.js.map +1 -1
- package/dist/plugin/plugin-repl.js.map +1 -1
- package/dist/plugin/system.js +7 -5
- package/dist/plugin/system.js.map +1 -1
- package/dist/rpc.js +13 -3
- package/dist/rpc.js.map +1 -1
- package/dist/runtime.js +18 -7
- package/dist/runtime.js.map +1 -1
- package/dist/scrypted-main.js +4 -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 +429 -0
- package/dist/scrypted-server-main.js.map +1 -0
- package/package.json +7 -6
- package/python/media.py +1 -12
- package/python/plugin-remote.py +22 -7
- package/python/rpc.py +5 -4
- package/src/http-interfaces.ts +12 -5
- package/src/level.ts +0 -2
- package/src/plugin/media.ts +4 -4
- package/src/plugin/plugin-api.ts +9 -1
- package/src/plugin/plugin-console.ts +0 -1
- package/src/plugin/plugin-device.ts +10 -3
- package/src/plugin/plugin-host-api.ts +2 -2
- package/src/plugin/plugin-host.ts +121 -264
- package/src/plugin/plugin-http.ts +3 -2
- package/src/plugin/plugin-lazy-remote.ts +1 -1
- package/src/plugin/plugin-remote-worker.ts +271 -0
- package/src/plugin/plugin-remote.ts +45 -16
- package/src/plugin/plugin-repl.ts +0 -1
- package/src/plugin/system.ts +8 -6
- package/src/rpc.ts +13 -2
- package/src/runtime.ts +18 -7
- package/src/scrypted-main.ts +5 -508
- package/src/scrypted-plugin-main.ts +6 -0
- package/src/scrypted-server-main.ts +498 -0
package/python/rpc.py
CHANGED
|
@@ -308,8 +308,8 @@ class RpcPeer:
|
|
|
308
308
|
|
|
309
309
|
result['result'] = self.serialize(value, False)
|
|
310
310
|
except Exception as e:
|
|
311
|
-
print('failure', method, e)
|
|
312
311
|
tb = traceback.format_exc()
|
|
312
|
+
print('failure', method, e, tb)
|
|
313
313
|
self.createErrorResult(
|
|
314
314
|
result, type(e).__name__, str(e), tb)
|
|
315
315
|
|
|
@@ -317,11 +317,12 @@ class RpcPeer:
|
|
|
317
317
|
self.send(result)
|
|
318
318
|
|
|
319
319
|
elif messageType == 'result':
|
|
320
|
-
|
|
320
|
+
id = message['id']
|
|
321
|
+
future = self.pendingResults.get(id, None)
|
|
321
322
|
if not future:
|
|
322
323
|
raise RpcResultException(
|
|
323
|
-
None, 'unknown result %s' %
|
|
324
|
-
del
|
|
324
|
+
None, 'unknown result %s' % id)
|
|
325
|
+
del self.pendingResults[id]
|
|
325
326
|
if hasattr(message, 'message') or hasattr(message, 'stack'):
|
|
326
327
|
e = RpcResultException(
|
|
327
328
|
None, message.get('message', None))
|
package/src/http-interfaces.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { HttpResponse, HttpResponseOptions } from "@scrypted/sdk/types";
|
|
2
2
|
import { Response } from "express";
|
|
3
3
|
import mime from "mime";
|
|
4
|
-
import
|
|
4
|
+
import { PROPERTY_PROXY_ONEWAY_METHODS } from "./rpc";
|
|
5
|
+
import { join as pathJoin } from 'path';
|
|
6
|
+
import fs from 'fs';
|
|
5
7
|
|
|
6
|
-
export function createResponseInterface(res: Response,
|
|
8
|
+
export function createResponseInterface(res: Response, unzippedDir: string): HttpResponse {
|
|
7
9
|
class HttpResponseImpl implements HttpResponse {
|
|
10
|
+
[PROPERTY_PROXY_ONEWAY_METHODS] = [
|
|
11
|
+
'send',
|
|
12
|
+
'sendFile',
|
|
13
|
+
];
|
|
14
|
+
|
|
8
15
|
send(body: string): void;
|
|
9
16
|
send(body: string, options: HttpResponseOptions): void;
|
|
10
17
|
send(body: Buffer): void;
|
|
@@ -35,13 +42,13 @@ export function createResponseInterface(res: Response, zip: AdmZip): HttpRespons
|
|
|
35
42
|
if (!res.getHeader('Content-Type'))
|
|
36
43
|
res.contentType(mime.lookup(path));
|
|
37
44
|
|
|
38
|
-
const
|
|
39
|
-
if (!
|
|
45
|
+
const filePath = pathJoin(unzippedDir, 'fs', path);
|
|
46
|
+
if (!fs.existsSync(filePath)) {
|
|
40
47
|
res.status(404);
|
|
41
48
|
res.end();
|
|
42
49
|
return;
|
|
43
50
|
}
|
|
44
|
-
res.
|
|
51
|
+
res.sendFile(filePath);
|
|
45
52
|
}
|
|
46
53
|
}
|
|
47
54
|
|
package/src/level.ts
CHANGED
package/src/plugin/media.ts
CHANGED
|
@@ -7,12 +7,12 @@ 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) {
|
|
14
14
|
mediaManager.builtinConverters.push({
|
|
15
|
-
fromMimeType: ScryptedMimeTypes.Url
|
|
15
|
+
fromMimeType: `${ScryptedMimeTypes.Url};${ScryptedMimeTypes.AcceptUrlParameter}=true`,
|
|
16
16
|
toMimeType: ScryptedMimeTypes.FFmpegInput,
|
|
17
17
|
async convert(data: string | Buffer, fromMimeType: string): Promise<Buffer | string> {
|
|
18
18
|
const url = data.toString();
|
|
@@ -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>;
|
|
@@ -8,6 +8,8 @@ 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;
|
|
@@ -382,9 +384,14 @@ export class PluginDeviceProxyHandler implements PrimitiveProxyHandler<any>, Scr
|
|
|
382
384
|
if (pluginDevice && !pluginDevice.nativeId) {
|
|
383
385
|
const plugin = this.scrypted.plugins[pluginDevice.pluginId];
|
|
384
386
|
if (!plugin.packageJson.scrypted.interfaces.includes(ScryptedInterface.Readme)) {
|
|
385
|
-
const
|
|
386
|
-
if (
|
|
387
|
-
|
|
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
|
+
}
|
|
388
395
|
}
|
|
389
396
|
}
|
|
390
397
|
}
|
|
@@ -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 { getPluginNodePath
|
|
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
99
|
NODE_PATH: path.join(getPluginNodePath(this.pluginId), 'node_modules'),
|
|
96
|
-
SCRYPTED_PLUGIN_VOLUME:
|
|
97
|
-
},
|
|
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,223 +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 reconnect = () => {
|
|
437
|
-
stdout.removeAllListeners();
|
|
438
|
-
stderr.removeAllListeners();
|
|
439
|
-
stdout.pause();
|
|
440
|
-
stderr.pause();
|
|
441
|
-
setTimeout(tryConnect, 10000);
|
|
442
|
-
};
|
|
443
|
-
|
|
444
|
-
const connect = async () => {
|
|
445
|
-
const ds = deviceManager.getDeviceState(nativeId);
|
|
446
|
-
if (!ds) {
|
|
447
|
-
// deleted?
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
const { pluginId, nativeId: mixinNativeId } = await plugins.getDeviceInfo(mixinId);
|
|
452
|
-
const port = await plugins.getRemoteServicePort(pluginId, 'console-writer');
|
|
453
|
-
const socket = net.connect(port);
|
|
454
|
-
socket.write(mixinNativeId + '\n');
|
|
455
|
-
const writer = (data: Buffer) => {
|
|
456
|
-
let str = data.toString().trim();
|
|
457
|
-
str = str.replaceAll('\n', `\n[${ds.name}]: `);
|
|
458
|
-
str = `[${ds.name}]: ` + str + '\n';
|
|
459
|
-
socket.write(str);
|
|
460
|
-
};
|
|
461
|
-
stdout.on('data', writer);
|
|
462
|
-
stderr.on('data', writer);
|
|
463
|
-
socket.on('close', reconnect);
|
|
464
|
-
};
|
|
465
|
-
|
|
466
|
-
const tryConnect = async() => {
|
|
467
|
-
try {
|
|
468
|
-
await connect();
|
|
469
|
-
}
|
|
470
|
-
catch (e) {
|
|
471
|
-
reconnect();
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
tryConnect();
|
|
475
|
-
}, getDeviceConsole(nativeId), `[${systemManager.getDeviceById(mixinId)?.name}]`);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
let lastCpuUsage: NodeJS.CpuUsage;
|
|
479
|
-
setInterval(() => {
|
|
480
|
-
const cpuUsage = process.cpuUsage(lastCpuUsage);
|
|
481
|
-
lastCpuUsage = cpuUsage;
|
|
482
|
-
peer.sendOob({
|
|
483
|
-
type: 'stats',
|
|
484
|
-
cpu: cpuUsage,
|
|
485
|
-
memoryUsage: process.memoryUsage(),
|
|
486
|
-
});
|
|
487
|
-
global?.gc();
|
|
488
|
-
}, 10000);
|
|
489
|
-
|
|
490
|
-
let replPort: Promise<number>;
|
|
491
|
-
|
|
492
|
-
let _pluginConsole: Console;
|
|
493
|
-
const getPluginConsole = () => {
|
|
494
|
-
if (!_pluginConsole)
|
|
495
|
-
_pluginConsole = getDeviceConsole(undefined);
|
|
496
|
-
return _pluginConsole;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
attachPluginRemote(peer, {
|
|
500
|
-
createMediaManager: async (sm) => {
|
|
501
|
-
systemManager = sm;
|
|
502
|
-
return new MediaManagerImpl(systemManager, getPluginConsole());
|
|
503
|
-
},
|
|
504
|
-
onGetRemote: async (_api, _pluginId) => {
|
|
505
|
-
api = _api;
|
|
506
|
-
pluginId = _pluginId;
|
|
507
|
-
peer.selfName = pluginId;
|
|
508
|
-
},
|
|
509
|
-
onPluginReady: async (scrypted, params, plugin) => {
|
|
510
|
-
replPort = createREPLServer(scrypted, params, plugin);
|
|
511
|
-
},
|
|
512
|
-
getPluginConsole,
|
|
513
|
-
getDeviceConsole,
|
|
514
|
-
getMixinConsole,
|
|
515
|
-
async getServicePort(name, ...args: any[]) {
|
|
516
|
-
if (name === 'repl') {
|
|
517
|
-
if (!replPort)
|
|
518
|
-
throw new Error('REPL unavailable: Plugin not loaded.')
|
|
519
|
-
return replPort;
|
|
520
|
-
}
|
|
521
|
-
throw new Error(`unknown service ${name}`);
|
|
522
|
-
},
|
|
523
|
-
async onLoadZip(zip: AdmZip, packageJson: any) {
|
|
524
|
-
installSourceMapSupport({
|
|
525
|
-
environment: 'node',
|
|
526
|
-
retrieveSourceMap(source) {
|
|
527
|
-
if (source === '/plugin/main.nodejs.js' || source === `/${pluginId}/main.nodejs.js`) {
|
|
528
|
-
const entry = zip.getEntry('main.nodejs.js.map')
|
|
529
|
-
const map = entry?.getData().toString();
|
|
530
|
-
if (!map)
|
|
531
|
-
return null;
|
|
532
|
-
return {
|
|
533
|
-
url: '/plugin/main.nodejs.js',
|
|
534
|
-
map,
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
return null;
|
|
538
|
-
}
|
|
539
|
-
});
|
|
540
|
-
await installOptionalDependencies(getPluginConsole(), packageJson);
|
|
541
|
-
}
|
|
542
|
-
}).then(scrypted => {
|
|
543
|
-
systemManager = scrypted.systemManager;
|
|
544
|
-
deviceManager = scrypted.deviceManager;
|
|
545
|
-
|
|
546
|
-
process.on('uncaughtException', e => {
|
|
547
|
-
getPluginConsole().error('uncaughtException', e);
|
|
548
|
-
scrypted.log.e('uncaughtException ' + e?.toString());
|
|
549
|
-
});
|
|
550
|
-
process.on('unhandledRejection', e => {
|
|
551
|
-
getPluginConsole().error('unhandledRejection', e);
|
|
552
|
-
scrypted.log.e('unhandledRejection ' + e?.toString());
|
|
553
|
-
});
|
|
554
|
-
})
|
|
555
|
-
}
|
|
@@ -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
|
|
|
@@ -53,6 +53,7 @@ export abstract class PluginHttp<T> {
|
|
|
53
53
|
|
|
54
54
|
if (!isPublicEndpoint && !res.locals.username) {
|
|
55
55
|
end(401, 'Not Authorized');
|
|
56
|
+
console.log('rejected request', isPublicEndpoint, res.locals.username, req.originalUrl)
|
|
56
57
|
return;
|
|
57
58
|
}
|
|
58
59
|
|