@scrypted/server 0.1.16 → 0.2.1

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 (38) hide show
  1. package/dist/event-registry.js +3 -4
  2. package/dist/event-registry.js.map +1 -1
  3. package/dist/plugin/media.js +53 -65
  4. package/dist/plugin/media.js.map +1 -1
  5. package/dist/plugin/plugin-api.js +1 -1
  6. package/dist/plugin/plugin-api.js.map +1 -1
  7. package/dist/plugin/plugin-device.js +25 -11
  8. package/dist/plugin/plugin-device.js.map +1 -1
  9. package/dist/plugin/plugin-host-api.js.map +1 -1
  10. package/dist/plugin/plugin-host.js +5 -2
  11. package/dist/plugin/plugin-host.js.map +1 -1
  12. package/dist/plugin/plugin-remote-worker.js +66 -24
  13. package/dist/plugin/plugin-remote-worker.js.map +1 -1
  14. package/dist/plugin/plugin-remote.js +14 -4
  15. package/dist/plugin/plugin-remote.js.map +1 -1
  16. package/dist/rpc.js +2 -2
  17. package/dist/rpc.js.map +1 -1
  18. package/dist/runtime.js +11 -10
  19. package/dist/runtime.js.map +1 -1
  20. package/dist/scrypted-server-main.js +8 -4
  21. package/dist/scrypted-server-main.js.map +1 -1
  22. package/dist/server-settings.js +5 -1
  23. package/dist/server-settings.js.map +1 -1
  24. package/dist/state.js +2 -1
  25. package/dist/state.js.map +1 -1
  26. package/package.json +4 -11
  27. package/src/event-registry.ts +3 -4
  28. package/src/plugin/media.ts +71 -84
  29. package/src/plugin/plugin-api.ts +4 -4
  30. package/src/plugin/plugin-device.ts +25 -11
  31. package/src/plugin/plugin-host-api.ts +1 -1
  32. package/src/plugin/plugin-host.ts +0 -1
  33. package/src/plugin/plugin-remote-worker.ts +90 -30
  34. package/src/plugin/plugin-remote.ts +17 -6
  35. package/src/rpc.ts +2 -1
  36. package/src/runtime.ts +6 -9
  37. package/src/scrypted-server-main.ts +8 -4
  38. 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 interfaces = mixinProvider?.interfaces?.includes(ScryptedInterface.MixinProvider) && await mixinProvider?.canMixin(type, allInterfaces) as any as ScryptedInterface[];
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
- console.log(`mixin provider ${mixinId} can no longer mixin ${this.id}`);
225
- const mixins: string[] = getState(pluginDevice, ScryptedInterfaceProperty.mixins) || [];
226
- this.scrypted.stateManager.setPluginDeviceState(pluginDevice, ScryptedInterfaceProperty.mixins, mixins.filter(mid => mid !== mixinId))
227
- this.scrypted.datastore.upsert(pluginDevice);
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
- const pluginDevice = this.scrypted.findPluginDeviceById(mixin.mixinProviderId || this.id);
357
- const plugin = this.scrypted.plugins[pluginDevice.pluginId];
358
- let methods = new Set<string>(getInterfaceMethods(ScryptedInterfaceDescriptors, entry.interfaces))
359
- if (plugin.api.descriptors)
360
- methods = new Set<string>([...methods, ...getInterfaceMethods(plugin.api.descriptors, entry.interfaces)]);
361
- entry.methods = methods;
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: any, eventData?: any) {
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
 
@@ -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...');
@@ -9,18 +9,24 @@ import { install as installSourceMapSupport } from 'source-map-support';
9
9
  import { PassThrough } from 'stream';
10
10
  import { RpcMessage, RpcPeer } from '../rpc';
11
11
  import { MediaManagerImpl } from './media';
12
- import { PluginAPI, PluginRemoteLoadZipOptions } from './plugin-api';
12
+ import { PluginAPI, PluginAPIProxy, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
13
13
  import { installOptionalDependencies } from './plugin-npm-dependencies';
14
- import { attachPluginRemote, PluginReader, setupPluginRemote } from './plugin-remote';
14
+ import { attachPluginRemote, DeviceManagerImpl, PluginReader, setupPluginRemote } from './plugin-remote';
15
15
  import { createREPLServer } from './plugin-repl';
16
16
  import { NodeThreadWorker } from './runtime/node-thread-worker';
17
17
  const { link } = require('linkfs');
18
18
 
19
+ interface PluginStats {
20
+ type: 'stats',
21
+ cpu: NodeJS.CpuUsage;
22
+ memoryUsage: NodeJS.MemoryUsage;
23
+ }
24
+
19
25
  export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessage, reject?: (e: Error) => void, serializationContext?: any) => void) {
20
26
  const peer = new RpcPeer('unknown', 'host', peerSend);
21
27
 
22
28
  let systemManager: SystemManager;
23
- let deviceManager: DeviceManager;
29
+ let deviceManager: DeviceManagerImpl;
24
30
  let api: PluginAPI;
25
31
 
26
32
  const getConsole = (hook: (stdout: PassThrough, stderr: PassThrough) => Promise<void>,
@@ -126,12 +132,6 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
126
132
  if (!mixinId) {
127
133
  return;
128
134
  }
129
- // todo: fix this. a mixin provider can mixin another device to make it a mixin provider itself.
130
- // so the mixin id in the mixin table will be incorrect.
131
- // there's no easy way to fix this from the remote.
132
- // if (!systemManager.getDeviceById(mixinId).mixins.includes(idForNativeId(nativeId))) {
133
- // return;
134
- // }
135
135
  const reconnect = () => {
136
136
  stdout.removeAllListeners();
137
137
  stderr.removeAllListeners();
@@ -178,15 +178,35 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
178
178
  return ret;
179
179
  }
180
180
 
181
- peer.getParam('updateStats').then((updateStats: (stats: any) => void) => {
182
- let lastCpuUsage: NodeJS.CpuUsage;
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) => {
183
186
  setInterval(() => {
184
- const cpuUsage = process.cpuUsage(lastCpuUsage);
185
- lastCpuUsage = cpuUsage;
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
+
186
206
  updateStats({
187
207
  type: 'stats',
188
208
  cpu: cpuUsage,
189
- memoryUsage: process.memoryUsage(),
209
+ memoryUsage,
190
210
  });
191
211
  }, 10000);
192
212
  });
@@ -316,35 +336,75 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
316
336
  pluginReader = undefined;
317
337
  const script = main.toString();
318
338
 
319
- scrypted.fork = async () => {
339
+
340
+ const forks = new Set<PluginRemote>();
341
+
342
+ scrypted.fork = () => {
320
343
  const ntw = new NodeThreadWorker(pluginId, {
321
344
  env: process.env,
322
345
  pluginDebug: undefined,
323
346
  });
324
- const threadPeer = new RpcPeer('main', 'thread', (message, reject) => ntw.send(message, reject));
325
- threadPeer.params.updateStats = (stats: any) => {
326
- // todo: merge.
327
- // this.stats = stats;
328
- }
329
- ntw.setupRpcPeer(threadPeer);
330
347
 
331
- const remote = await setupPluginRemote(threadPeer, api, pluginId, () => systemManager.getSystemState());
332
- const forkOptions = Object.assign({}, zipOptions);
333
- forkOptions.fork = true;
334
- return remote.loadZip(packageJson, zipData, forkOptions)
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
+ }
335
394
  }
336
395
 
337
396
  try {
338
397
  peer.evalLocal(script, zipOptions?.filename || '/plugin/main.nodejs.js', params);
339
- pluginConsole?.log('plugin successfully loaded');
340
398
 
341
399
  if (zipOptions?.fork) {
400
+ pluginConsole?.log('plugin forked');
342
401
  const fork = exports.fork;
343
- const ret = await fork();
344
- ret[RpcPeer.PROPERTY_JSON_DISABLE_SERIALIZATION] = true;
345
- return ret;
402
+ const forked = await fork();
403
+ forked[RpcPeer.PROPERTY_JSON_DISABLE_SERIALIZATION] = true;
404
+ return forked;
346
405
  }
347
406
 
407
+ pluginConsole?.log('plugin loaded');
348
408
  let pluginInstance = exports.default;
349
409
  // support exporting a plugin class, plugin main function,
350
410
  // or a plugin instance
@@ -365,7 +425,7 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
365
425
  }
366
426
  }).then(scrypted => {
367
427
  systemManager = scrypted.systemManager;
368
- deviceManager = scrypted.deviceManager;
428
+ deviceManager = scrypted.deviceManager as DeviceManagerImpl;
369
429
  });
370
430
 
371
431
  return peer;
@@ -107,6 +107,10 @@ class DeviceStateProxyHandler implements ProxyHandler<any> {
107
107
  get?(target: any, p: PropertyKey, receiver: any) {
108
108
  if (p === 'id')
109
109
  return this.id;
110
+ if (p === RpcPeer.PROPERTY_PROXY_PROPERTIES)
111
+ return { id: this.id }
112
+ if (p === 'setState')
113
+ return this.setState;
110
114
  return this.deviceManager.systemManager.state[this.id][p as string]?.value;
111
115
  }
112
116
 
@@ -128,7 +132,7 @@ interface DeviceManagerDevice {
128
132
  storage: { [key: string]: any };
129
133
  }
130
134
 
131
- class DeviceManagerImpl implements DeviceManager {
135
+ export class DeviceManagerImpl implements DeviceManager {
132
136
  api: PluginAPI;
133
137
  nativeIds = new Map<string, DeviceManagerDevice>();
134
138
  deviceStorage = new Map<string, StorageImpl>();
@@ -153,6 +157,11 @@ class DeviceManagerImpl implements DeviceManager {
153
157
  return new Proxy(handler, handler);
154
158
  }
155
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
+
156
165
  getDeviceStorage(nativeId?: any): StorageImpl {
157
166
  let ret = this.deviceStorage.get(nativeId);
158
167
  if (!ret) {
@@ -297,7 +306,7 @@ export async function setupPluginRemote(peer: RpcPeer, api: PluginAPI, pluginId:
297
306
  if (!peer.constructorSerializerMap.get(Buffer))
298
307
  peer.addSerializer(Buffer, 'Buffer', new BufferSerializer());
299
308
  const getRemote = await peer.getParam('getRemote');
300
- const remote = await getRemote(api, pluginId);
309
+ const remote = await getRemote(api, pluginId) as PluginRemote;
301
310
 
302
311
  await remote.setSystemState(getSystemState());
303
312
  api.listen((id, eventDetails, eventData) => {
@@ -337,7 +346,7 @@ export interface WebSocketCustomHandler {
337
346
  export type PluginReader = (name: string) => Buffer;
338
347
 
339
348
  export interface PluginRemoteAttachOptions {
340
- createMediaManager?: (systemManager: SystemManager, deviceManager: DeviceManager) => Promise<MediaManager>;
349
+ createMediaManager?: (systemManager: SystemManager, deviceManager: DeviceManagerImpl) => Promise<MediaManager>;
341
350
  getServicePort?: (name: string, ...args: any[]) => Promise<number>;
342
351
  getDeviceConsole?: (nativeId?: ScryptedNativeId) => Console;
343
352
  getPluginConsole?: () => Console;
@@ -381,6 +390,7 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
381
390
  mediaManager,
382
391
  log,
383
392
  pluginHostAPI: api,
393
+ pluginRemoteAPI: undefined,
384
394
  }
385
395
 
386
396
  delete peer.params.getRemote;
@@ -402,9 +412,8 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
402
412
  'setNativeId',
403
413
  ],
404
414
  getServicePort,
405
- createDeviceState(id: string, setState: (property: string, value: any) => Promise<void>) {
406
- const handler = new DeviceStateProxyHandler(deviceManager, id, setState);
407
- return new Proxy(handler, handler);
415
+ async createDeviceState(id: string, setState: (property: string, value: any) => Promise<void>) {
416
+ return deviceManager.createDeviceState(id, setState);
408
417
  },
409
418
 
410
419
  async ioEvent(id: string, event: string, message?: any) {
@@ -507,6 +516,8 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
507
516
  },
508
517
  }
509
518
 
519
+ ret.pluginRemoteAPI = remote;
520
+
510
521
  return remote;
511
522
  }
512
523
 
package/src/rpc.ts CHANGED
@@ -321,7 +321,8 @@ export class RpcPeer {
321
321
  const params = Object.assign({}, this.params, coercedParams);
322
322
  let compile: CompileFunction;
323
323
  try {
324
- compile = require('vm').compileFunction;;
324
+ // prevent bundlers from trying to include non-existent vm module.
325
+ compile = module[`require`]('vm').compileFunction;
325
326
  }
326
327
  catch (e) {
327
328
  compile = compileFunction;
package/src/runtime.ts CHANGED
@@ -8,7 +8,6 @@ import http, { ServerResponse } from 'http';
8
8
  import https from 'https';
9
9
  import { spawn as ptySpawn } from 'node-pty-prebuilt-multiarch';
10
10
  import path from 'path';
11
- import qs from "query-string";
12
11
  import rimraf from 'rimraf';
13
12
  import semver from 'semver';
14
13
  import { PassThrough } from 'stream';
@@ -184,12 +183,10 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
184
183
 
185
184
  const url = new URL(callback_url as string);
186
185
  if (url.search) {
187
- const search = qs.parse(url.search);
188
- const state = search.state as string;
186
+ const state = url.searchParams.get('state');
189
187
  if (state) {
190
188
  const { s, d, r } = JSON.parse(state);
191
- search.state = s;
192
- url.search = '?' + qs.stringify(search);
189
+ url.searchParams.set('state', s);
193
190
  const oauthClient: ScryptedDevice & OauthClient = this.getDevice(d);
194
191
  await oauthClient.onOauthCallback(url.toString()).catch();
195
192
  res.redirect(r);
@@ -197,12 +194,12 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
197
194
  }
198
195
  }
199
196
  if (url.hash) {
200
- const hash = qs.parse(url.hash);
201
- const state = hash.state as string;
197
+ const hash = new URLSearchParams(url.hash.substring(1));
198
+ const state = hash.get('state');
202
199
  if (state) {
203
200
  const { s, d, r } = JSON.parse(state);
204
- hash.state = s;
205
- url.hash = '#' + qs.stringify(hash);
201
+ hash.set('state', s);
202
+ url.hash = '#' + hash.toString();
206
203
  const oauthClient: ScryptedDevice & OauthClient = this.getDevice(d);
207
204
  await oauthClient.onOauthCallback(url.toString());
208
205
  res.redirect(r);
@@ -12,7 +12,6 @@ import { getHostAddresses, SCRYPTED_DEBUG_PORT, SCRYPTED_INSECURE_PORT, SCRYPTED
12
12
  import crypto from 'crypto';
13
13
  import cookieParser from 'cookie-parser';
14
14
  import axios from 'axios';
15
- import qs from 'query-string';
16
15
  import { RPCResultError } from './rpc';
17
16
  import fs from 'fs';
18
17
  import mkdirp from 'mkdirp';
@@ -318,8 +317,8 @@ async function start() {
318
317
 
319
318
  app.get('/web/component/script/search', async (req, res) => {
320
319
  try {
321
- const query = qs.stringify({
322
- text: req.query.text,
320
+ const query = new URLSearchParams({
321
+ text: req.query.text.toString(),
323
322
  })
324
323
  const response = await axios(`https://registry.npmjs.org/-/v1/search?${query}`);
325
324
  res.send(response.data);
@@ -405,7 +404,12 @@ async function start() {
405
404
 
406
405
  app.get('/logout', (req, res) => {
407
406
  res.clearCookie(getLoginUserToken(req.secure));
408
- res.send({});
407
+ if (req.headers['accept']?.startsWith('application/json')) {
408
+ res.send({});
409
+ }
410
+ else {
411
+ res.redirect('/endpoint/@scrypted/core/public/');
412
+ }
409
413
  });
410
414
 
411
415
  let hasLogin = await db.getCount(ScryptedUser) > 0;
package/src/state.ts CHANGED
@@ -116,7 +116,7 @@ export class ScryptedStateManager extends EventRegistry {
116
116
  let cb = (eventDetails: EventDetails, eventData: any) => {
117
117
  if (denoise && lastData === eventData)
118
118
  return;
119
- callback(eventDetails, eventData);
119
+ callback?.(eventDetails, eventData);
120
120
  };
121
121
 
122
122
  const wrappedRegister = super.listenDevice(id, options, cb);
@@ -124,6 +124,7 @@ export class ScryptedStateManager extends EventRegistry {
124
124
  return new EventListenerRegisterImpl(() => {
125
125
  wrappedRegister.removeListener();
126
126
  cb = undefined;
127
+ callback = undefined;
127
128
  polling = false;
128
129
  });
129
130
  }