@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.

Files changed (56) 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 +4 -4
  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 +13 -3
  9. package/dist/plugin/plugin-device.js.map +1 -1
  10. package/dist/plugin/plugin-host-api.js +2 -2
  11. package/dist/plugin/plugin-host-api.js.map +1 -1
  12. package/dist/plugin/plugin-host.js +110 -235
  13. package/dist/plugin/plugin-host.js.map +1 -1
  14. package/dist/plugin/plugin-http.js +1 -0
  15. package/dist/plugin/plugin-http.js.map +1 -1
  16. package/dist/plugin/plugin-lazy-remote.js.map +1 -1
  17. package/dist/plugin/plugin-remote-worker.js +252 -0
  18. package/dist/plugin/plugin-remote-worker.js.map +1 -0
  19. package/dist/plugin/plugin-remote.js +38 -14
  20. package/dist/plugin/plugin-remote.js.map +1 -1
  21. package/dist/plugin/plugin-repl.js.map +1 -1
  22. package/dist/plugin/system.js +7 -5
  23. package/dist/plugin/system.js.map +1 -1
  24. package/dist/rpc.js +13 -3
  25. package/dist/rpc.js.map +1 -1
  26. package/dist/runtime.js +18 -7
  27. package/dist/runtime.js.map +1 -1
  28. package/dist/scrypted-main.js +4 -437
  29. package/dist/scrypted-main.js.map +1 -1
  30. package/dist/scrypted-plugin-main.js +8 -0
  31. package/dist/scrypted-plugin-main.js.map +1 -0
  32. package/dist/scrypted-server-main.js +429 -0
  33. package/dist/scrypted-server-main.js.map +1 -0
  34. package/package.json +7 -6
  35. package/python/media.py +1 -12
  36. package/python/plugin-remote.py +22 -7
  37. package/python/rpc.py +5 -4
  38. package/src/http-interfaces.ts +12 -5
  39. package/src/level.ts +0 -2
  40. package/src/plugin/media.ts +4 -4
  41. package/src/plugin/plugin-api.ts +9 -1
  42. package/src/plugin/plugin-console.ts +0 -1
  43. package/src/plugin/plugin-device.ts +10 -3
  44. package/src/plugin/plugin-host-api.ts +2 -2
  45. package/src/plugin/plugin-host.ts +121 -264
  46. package/src/plugin/plugin-http.ts +3 -2
  47. package/src/plugin/plugin-lazy-remote.ts +1 -1
  48. package/src/plugin/plugin-remote-worker.ts +271 -0
  49. package/src/plugin/plugin-remote.ts +45 -16
  50. package/src/plugin/plugin-repl.ts +0 -1
  51. package/src/plugin/system.ts +8 -6
  52. package/src/rpc.ts +13 -2
  53. package/src/runtime.ts +18 -7
  54. package/src/scrypted-main.ts +5 -508
  55. package/src/scrypted-plugin-main.ts +6 -0
  56. 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
- future = self.pendingResults.get(message['id'], None)
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' % message['id'])
324
- del message['id']
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))
@@ -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 AdmZip from "adm-zip";
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, zip: AdmZip): HttpResponse {
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 data = zip.getEntry(`fs/${path}`)?.getData();
39
- if (!data) {
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.send(data);
51
+ res.sendFile(filePath);
45
52
  }
46
53
  }
47
54
 
package/src/level.ts CHANGED
@@ -5,8 +5,6 @@
5
5
  // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
6
6
  // TypeScript Version: 2.3
7
7
 
8
- import { asyncFilter } from "./asynciterable-utils";
9
-
10
8
  export interface AbstractOptions {
11
9
  // wtf is this?
12
10
  readonly [k: string]: any;
@@ -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 pathToFfmpeg from 'ffmpeg-for-homebridge';
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 + ';' + ScryptedMimeTypes.AcceptUrlParameter,
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 pathToFfmpeg || defaultPath;
136
+ return getInstalledFfmpeg() || defaultPath;
137
137
  }
138
138
 
139
139
  getConverters(): BufferConverter[] {
@@ -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>;
@@ -1,5 +1,4 @@
1
1
  import { ScryptedNativeId } from '@scrypted/sdk/types'
2
- import { EventEmitter } from 'ws';
3
2
  import { listenZero } from './listen-zero';
4
3
  import { Server } from 'net';
5
4
  import { once } from 'events';
@@ -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 entry = plugin.zip.getEntry('README.md');
386
- if (entry) {
387
- return entry.getData().toString();
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, plugin: Plugin, public pluginHost: PluginHost, public mediaManager: MediaManager) {
38
+ constructor(public scrypted: ScryptedRuntime, pluginId: string, public pluginHost: PluginHost, public mediaManager: MediaManager) {
39
39
  super();
40
- this.pluginId = plugin._id;
40
+ this.pluginId = pluginId;
41
41
  }
42
42
 
43
43
  // do we care about mixin validation here?
@@ -1,39 +1,38 @@
1
- import { RpcMessage, RpcPeer } from '../rpc';
1
+ import { RpcPeer } from '../rpc';
2
2
  import AdmZip from 'adm-zip';
3
- import { SystemManager, DeviceManager, ScryptedNativeId, Device, EventListenerRegister, EngineIOHandler, ScryptedInterface, ScryptedInterfaceProperty } from '@scrypted/sdk/types'
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 { attachPluginRemote, setupPluginRemote } from './plugin-remote';
8
- import { PluginAPI, PluginAPIProxy, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
7
+ import { setupPluginRemote } from './plugin-remote';
8
+ import { PluginAPIProxy, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
9
9
  import { Logger } from '../logger';
10
- import { MediaManagerHostImpl, MediaManagerImpl } from './media';
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, installOptionalDependencies } from './plugin-npm-dependencies';
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
- const logger = scrypted.getDeviceLogger(scrypted.findPluginDevice(plugin._id));
89
+ let zipBuffer = Buffer.from(plugin.zip, 'base64');
90
+ // allow garbage collection of the base 64 contents
91
+ plugin = undefined;
90
92
 
91
- const volume = path.join(process.cwd(), 'volume');
92
- const cwd = ensurePluginVolume(this.pluginId);
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: cwd,
97
- }, plugin.packageJson.scrypted.runtime);
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, plugin, this, mediaManager);
142
-
143
- const zipBuffer = Buffer.from(plugin.zip, 'base64');
144
- this.zip = new AdmZip(zipBuffer);
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: runtime === 'python'
197
+ filename: isPython
177
198
  ? pluginDebug
178
199
  ? `${volume}/plugin.zip`
179
- : `${cwd}/plugin.zip`
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
- const module = await remote.loadZip(plugin.packageJson, zipBuffer, loadZipOptions);
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
- this.worker = child_process.fork(require.main.filename, ['child'], {
264
- stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
265
- env: Object.assign({}, process.env, env),
266
- serialization: 'advanced',
267
- execArgv,
268
- });
269
-
270
- this.peer = new RpcPeer('host', this.pluginId, (message, reject) => {
271
- if (connected) {
272
- this.worker.send(message, undefined, e => {
273
- if (e && reject)
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
- else if (reject) {
278
- reject(new Error('peer disconnected'));
279
- }
280
- });
281
- this.peer.transportSafeArgumentTypes.add(Buffer.name);
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.worker.on('message', message => this.peer.handleMessage(message as any));
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): Promise<void>;
32
- abstract handleRequestEndpoint(req: Request, res: Response, endpointRequest: HttpRequest, pluginData: T): Promise<void>;
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