@scrypted/server 0.1.7 → 0.1.10

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 (46) hide show
  1. package/dist/listen-zero.js +38 -0
  2. package/dist/listen-zero.js.map +1 -0
  3. package/dist/plugin/media.js +1 -1
  4. package/dist/plugin/media.js.map +1 -1
  5. package/dist/plugin/plugin-console.js +1 -1
  6. package/dist/plugin/plugin-console.js.map +1 -1
  7. package/dist/plugin/plugin-host.js +33 -45
  8. package/dist/plugin/plugin-host.js.map +1 -1
  9. package/dist/plugin/plugin-http.js +1 -1
  10. package/dist/plugin/plugin-http.js.map +1 -1
  11. package/dist/plugin/plugin-npm-dependencies.js +3 -1
  12. package/dist/plugin/plugin-npm-dependencies.js.map +1 -1
  13. package/dist/plugin/plugin-repl.js +1 -1
  14. package/dist/plugin/plugin-repl.js.map +1 -1
  15. package/dist/plugin/runtime/node-fork-worker.js +8 -1
  16. package/dist/plugin/runtime/node-fork-worker.js.map +1 -1
  17. package/dist/rpc-serializer.js +121 -0
  18. package/dist/rpc-serializer.js.map +1 -0
  19. package/dist/runtime.js +0 -36
  20. package/dist/runtime.js.map +1 -1
  21. package/dist/scrypted-server-main.js +0 -2
  22. package/dist/scrypted-server-main.js.map +1 -1
  23. package/dist/services/plugin.js +7 -10
  24. package/dist/services/plugin.js.map +1 -1
  25. package/dist/threading.js +6 -28
  26. package/dist/threading.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/listen-zero.ts +34 -0
  29. package/src/plugin/media.ts +1 -1
  30. package/src/plugin/plugin-console.ts +1 -1
  31. package/src/plugin/plugin-host.ts +42 -43
  32. package/src/plugin/plugin-http.ts +2 -2
  33. package/src/plugin/plugin-npm-dependencies.ts +3 -1
  34. package/src/plugin/plugin-repl.ts +1 -1
  35. package/src/plugin/runtime/node-fork-worker.ts +8 -1
  36. package/src/plugin/runtime/runtime-worker.ts +2 -1
  37. package/src/rpc-serializer.ts +141 -0
  38. package/src/runtime.ts +0 -40
  39. package/src/scrypted-server-main.ts +0 -2
  40. package/src/services/plugin.ts +7 -10
  41. package/src/threading.ts +8 -35
  42. package/test/rpc-duplex-test.ts +30 -0
  43. package/test/threading-test.ts +29 -0
  44. package/dist/plugin/listen-zero.js +0 -23
  45. package/dist/plugin/listen-zero.js.map +0 -1
  46. package/src/plugin/listen-zero.ts +0 -21
@@ -5,6 +5,7 @@ import { ServerResponse, IncomingHttpHeaders } from 'http';
5
5
  import WebSocket, { Server as WebSocketServer } from "ws";
6
6
 
7
7
  export function isConnectionUpgrade(headers: IncomingHttpHeaders) {
8
+ // connection:'keep-alive, Upgrade'
8
9
  return headers.connection?.toLowerCase().includes('upgrade');
9
10
  }
10
11
 
@@ -34,7 +35,7 @@ export abstract class PluginHttp<T> {
34
35
  this.endpointHandler(req, res, false, false, this.handleRequestEndpoint.bind(this))
35
36
  });
36
37
  }
37
-
38
+
38
39
  abstract handleEngineIOEndpoint(req: Request, res: ServerResponse, endpointRequest: HttpRequest, pluginData: T): void;
39
40
  abstract handleRequestEndpoint(req: Request, res: Response, endpointRequest: HttpRequest, pluginData: T): void;
40
41
  abstract getEndpointPluginData(endpoint: string, isUpgrade: boolean, isEngineIOEndpoint: boolean): Promise<T>;
@@ -43,7 +44,6 @@ export abstract class PluginHttp<T> {
43
44
  async endpointHandler(req: Request, res: Response, isPublicEndpoint: boolean, isEngineIOEndpoint: boolean,
44
45
  handler: (req: Request, res: Response, endpointRequest: HttpRequest, pluginData: T) => void) {
45
46
 
46
- // connection:'keep-alive, Upgrade'
47
47
  const isUpgrade = isConnectionUpgrade(req.headers);
48
48
 
49
49
  const end = (code: number, message: string) => {
@@ -5,10 +5,12 @@ import path from 'path';
5
5
  import { once } from 'events';
6
6
  import process from 'process';
7
7
  import mkdirp from "mkdirp";
8
+ import semver from 'semver';
8
9
 
9
10
  export function getPluginNodePath(name: string) {
10
11
  const pluginVolume = ensurePluginVolume(name);
11
- const nodePrefix = path.join(pluginVolume, `${process.platform}-${process.arch}`);
12
+ const nodeMajorVersion = semver.parse(process.version).major;
13
+ const nodePrefix = path.join(pluginVolume, `${process.platform}-${process.arch}-${nodeMajorVersion}`);
12
14
  return nodePrefix;
13
15
  }
14
16
 
@@ -1,4 +1,4 @@
1
- import { listenZero } from './listen-zero';
1
+ import { listenZero } from '../listen-zero';
2
2
  import { Server } from 'net';
3
3
  import { once } from 'events';
4
4
  import repl from 'repl';
@@ -30,7 +30,14 @@ export class NodeForkWorker extends ChildProcessWorker {
30
30
  }
31
31
 
32
32
  setupRpcPeer(peer: RpcPeer): void {
33
- this.worker.on('message', message => peer.handleMessage(message as any));
33
+ this.worker.on('message', (message, sendHandle) => {
34
+ if (sendHandle) {
35
+ this.emit('rpc', message, sendHandle);
36
+ }
37
+ else {
38
+ peer.handleMessage(message as any);
39
+ }
40
+ });
34
41
  peer.transportSafeArgumentTypes.add(Buffer.name);
35
42
  }
36
43
 
@@ -1,6 +1,7 @@
1
1
  import { RpcMessage, RpcPeer } from "../../rpc";
2
2
  import { PluginDebug } from "../plugin-debug";
3
3
  import {Readable} from "stream";
4
+ import net from "net";
4
5
 
5
6
  export interface RuntimeWorkerOptions {
6
7
  pluginDebug: PluginDebug;
@@ -15,6 +16,7 @@ export interface RuntimeWorker {
15
16
 
16
17
  kill(): void;
17
18
 
19
+ on(event: 'rpc', listener: (message: any, sendHandle: net.Socket) => void): this;
18
20
  on(event: 'error', listener: (err: Error) => void): this;
19
21
  on(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
20
22
  on(event: 'disconnect', listener: () => void): this;
@@ -25,4 +27,3 @@ export interface RuntimeWorker {
25
27
 
26
28
  setupRpcPeer(peer: RpcPeer): void;
27
29
  }
28
-
@@ -0,0 +1,141 @@
1
+ import type { Readable, Writable } from "stream";
2
+ import { SidebandBufferSerializer } from "./plugin/buffer-serializer";
3
+ import { RpcPeer } from "./rpc";
4
+
5
+ export function createDuplexRpcPeer(selfName: string, peerName: string, readable: Readable, writable: Writable) {
6
+ const serializer = createRpcDuplexSerializer(readable, writable);
7
+
8
+ const rpcPeer = new RpcPeer(selfName, peerName, (message, reject, serializationContext) => {
9
+ try {
10
+ serializer.sendMessage(message, reject, serializationContext);
11
+ }
12
+ catch (e) {
13
+ reject?.(e);
14
+ readable.destroy();
15
+ }
16
+ });
17
+
18
+ serializer.setupRpcPeer(rpcPeer);
19
+ readable.on('close', serializer.onDisconnected);
20
+ readable.on('error', serializer.onDisconnected);
21
+ return rpcPeer;
22
+ }
23
+
24
+ export function createRpcSerializer(options: {
25
+ sendMessageBuffer: (buffer: Buffer) => void,
26
+ sendMessageFinish: (message: any) => void,
27
+ }) {
28
+ let rpcPeer: RpcPeer;
29
+
30
+ const { sendMessageBuffer, sendMessageFinish } = options;
31
+ let connected = true;
32
+ const onDisconnected = () => {
33
+ connected = false;
34
+ rpcPeer.kill('connection closed.');
35
+ }
36
+
37
+ const sendMessage = (message: any, reject: (e: Error) => void, serializationContext: any, ) => {
38
+ if (!connected) {
39
+ reject?.(new Error('peer disconnected'));
40
+ return;
41
+ }
42
+
43
+ const buffers = serializationContext?.buffers;
44
+ if (buffers) {
45
+ for (const buffer of buffers) {
46
+ sendMessageBuffer(buffer);
47
+ }
48
+ }
49
+ sendMessageFinish(message);
50
+ }
51
+
52
+ let pendingSerializationContext: any = {};
53
+ const setupRpcPeer = (peer: RpcPeer) => {
54
+ rpcPeer = peer;
55
+ rpcPeer.addSerializer(Buffer, 'Buffer', new SidebandBufferSerializer());
56
+ }
57
+
58
+ const onMessageBuffer = (buffer: Buffer) => {
59
+ pendingSerializationContext = pendingSerializationContext || {
60
+ buffers: [],
61
+ };
62
+ const buffers: Buffer[] = pendingSerializationContext.buffers;
63
+ buffers.push(buffer);
64
+ };
65
+
66
+ const onMessageFinish = (message: any) => {
67
+ const messageSerializationContext = pendingSerializationContext;
68
+ pendingSerializationContext = undefined;
69
+ rpcPeer.handleMessage(message, messageSerializationContext);
70
+ }
71
+
72
+ return {
73
+ sendMessage,
74
+ setupRpcPeer,
75
+ onMessageBuffer,
76
+ onMessageFinish,
77
+ onDisconnected,
78
+ };
79
+ }
80
+
81
+ export function createRpcDuplexSerializer(readable: Readable, writable: Writable) {
82
+ const socketSend = (type: number, data: Buffer) => {
83
+ const header = Buffer.alloc(5);
84
+ header.writeUInt32BE(data.length + 1, 0);
85
+ header.writeUInt8(type, 4);
86
+
87
+ writable.write(Buffer.concat([header, data]));
88
+ }
89
+
90
+ const createSocketSend = (type: number) => {
91
+ return (data: Buffer) => {
92
+ return socketSend(type, data);
93
+ }
94
+ }
95
+
96
+ const sendMessageBuffer = createSocketSend(1);
97
+ const sendMessageFinish = createSocketSend(0);
98
+
99
+ const serializer = createRpcSerializer({
100
+ sendMessageBuffer,
101
+ sendMessageFinish: (message) => sendMessageFinish(Buffer.from(JSON.stringify(message))),
102
+ });
103
+
104
+ let header: Buffer;
105
+ const readMessages = () => {
106
+ while (true) {
107
+ if (!header) {
108
+ header = readable.read(5);
109
+ if (!header)
110
+ return;
111
+ }
112
+
113
+ const length = header.readUInt32BE(0);
114
+ const type = header.readUInt8(4);
115
+ const payload: Buffer = readable.read(length - 1);
116
+ if (!payload)
117
+ return;
118
+
119
+ header = undefined;
120
+
121
+ const data = payload;
122
+
123
+ if (type === 0) {
124
+ const message = JSON.parse(data.toString());
125
+ serializer.onMessageFinish(message);
126
+ }
127
+ else {
128
+ serializer.onMessageBuffer(data);
129
+ }
130
+ }
131
+ }
132
+
133
+ readable.on('readable', readMessages);
134
+ readMessages();
135
+
136
+ return {
137
+ setupRpcPeer: serializer.setupRpcPeer,
138
+ sendMessage: serializer.sendMessage,
139
+ onDisconnected: serializer.onDisconnected,
140
+ };
141
+ }
package/src/runtime.ts CHANGED
@@ -78,46 +78,6 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
78
78
 
79
79
  app.disable('x-powered-by');
80
80
 
81
- this.app.options(['/endpoint/@:owner/:pkg/engine.io/api/activate', '/endpoint/@:owner/:pkg/engine.io/api/activate'], (req, res) => {
82
- this.addAccessControlHeaders(req, res);
83
- res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
84
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With');
85
- res.send(200);
86
- });
87
-
88
- this.app.post(['/endpoint/@:owner/:pkg/engine.io/api/activate', '/endpoint/@:owner/:pkg/engine.io/api/activate'], (req, res) => {
89
- const { username } = (req as any);
90
- if (!username) {
91
- res.status(401);
92
- res.send('Not Authorized');
93
- return;
94
- }
95
-
96
- const { owner, pkg } = req.params;
97
- let endpoint = pkg;
98
- if (owner)
99
- endpoint = `@${owner}/${endpoint}`;
100
-
101
- const { id } = req.body;
102
- try {
103
- const host = this.plugins?.[endpoint];
104
- if (!host)
105
- throw new Error('invalid plugin');
106
- // @ts-expect-error
107
- const socket: IOServerSocket = host.io.clients[id];
108
- if (!socket)
109
- throw new Error('invalid socket');
110
- socket.emit('/api/activate');
111
- res.send({
112
- id,
113
- })
114
- }
115
- catch (e) {
116
- res.status(500);
117
- res.end();
118
- }
119
- });
120
-
121
81
  this.addMiddleware();
122
82
 
123
83
  app.get('/web/oauth/callback', (req, res) => {
@@ -411,7 +411,6 @@ async function start() {
411
411
  secure: req.secure,
412
412
  signed: true,
413
413
  httpOnly: true,
414
- sameSite: !req.secure ? true : 'none',
415
414
  });
416
415
 
417
416
  if (change_password) {
@@ -453,7 +452,6 @@ async function start() {
453
452
  secure: req.secure,
454
453
  signed: true,
455
454
  httpOnly: true,
456
- sameSite: !req.secure ? true : 'none',
457
455
  });
458
456
 
459
457
  res.send({
@@ -51,26 +51,20 @@ export class PluginComponent {
51
51
  this.scrypted.getDevice(id);
52
52
  await this.scrypted.devices[id]?.handler?.ensureProxy();
53
53
  }
54
- async getMixins(id: string) {
55
- console.warn('legacy use of getMixins, use the mixins property');
56
- const pluginDevice = this.scrypted.findPluginDeviceById(id);
57
- return getState(pluginDevice, ScryptedInterfaceProperty.mixins) || [];
58
- }
59
54
  async getIdForPluginId(pluginId: string) {
60
55
  return this.scrypted.findPluginDevice(pluginId)?._id;
61
56
  }
62
57
  async getIdForNativeId(pluginId: string, nativeId: ScryptedNativeId) {
63
58
  return this.scrypted.findPluginDevice(pluginId, nativeId)?._id;
64
59
  }
60
+ /**
61
+ * @deprecated available as device.pluginId now.
62
+ * Remove at some point after core/ui rolls out 6/20/2022.
63
+ */
65
64
  async getPluginId(id: string) {
66
65
  const pluginDevice = this.scrypted.findPluginDeviceById(id);
67
66
  return pluginDevice.pluginId;
68
67
  }
69
- async getPluginProcessId(pluginId: string) {
70
- if (this.scrypted.plugins[pluginId]?.worker?.killed)
71
- return 'killed';
72
- return this.scrypted.plugins[pluginId]?.worker?.pid;
73
- }
74
68
  async reload(pluginId: string) {
75
69
  const plugin = await this.scrypted.datastore.tryGet(Plugin, pluginId);
76
70
  await this.scrypted.runPlugin(plugin);
@@ -94,14 +88,17 @@ export class PluginComponent {
94
88
  const packageJson = await this.getPackageJson(pluginId);
95
89
  const host = this.scrypted.plugins[pluginId];
96
90
  let rpcObjects = 0;
91
+ let pendingResults = 0;
97
92
  if (host.peer) {
98
93
  rpcObjects = host.peer.localProxied.size + Object.keys(host.peer.remoteWeakProxies).length;
94
+ pendingResults = Object.keys(host.peer.pendingResults).length;
99
95
  }
100
96
  return {
101
97
  pid: host?.worker?.pid,
102
98
  stats: host?.stats,
103
99
  rpcObjects,
104
100
  packageJson,
101
+ pendingResults,
105
102
  id: this.scrypted.findPluginDevice(pluginId)._id,
106
103
  }
107
104
  }
package/src/threading.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import worker_threads from 'worker_threads';
2
2
  import { getEvalSource, RpcPeer } from './rpc';
3
3
  import v8 from 'v8';
4
+ import vm from 'vm';
4
5
 
5
6
  export async function newThread<T>(thread: () => Promise<T>): Promise<T>;
6
7
  export async function newThread<V, T>(params: V, thread: (params: V) => Promise<T>): Promise<T>;
@@ -28,22 +29,22 @@ export async function newThread<T>(...args: any[]): Promise<T> {
28
29
  const g = global as any;
29
30
  g[customRequire] = g.require;
30
31
  }
31
- const v8 = global.require('v8');
32
- const worker_threads = global.require('worker_threads');
33
- const vm = global.require('vm');
34
- const mainPeer = new RpcPeer('thread', 'main', (message: any, reject: any) => {
32
+ const thread_v8: typeof v8 = global.require('v8');
33
+ const thread_worker_threads: typeof worker_threads = global.require('worker_threads');
34
+ const thread_vm: typeof vm = global.require('vm');
35
+ const mainPeer: RpcPeer = new RpcPeer('thread', 'main', (message: any, reject: any) => {
35
36
  try {
36
- worker_threads.parentPort.postMessage(v8.serialize(message));
37
+ thread_worker_threads.parentPort.postMessage(thread_v8.serialize(message));
37
38
  }
38
39
  catch (e) {
39
40
  reject?.(e);
40
41
  }
41
42
  });
42
43
  mainPeer.transportSafeArgumentTypes.add(Buffer.name);
43
- worker_threads.parentPort.on('message', (message: any) => mainPeer.handleMessage(v8.deserialize(message)));
44
+ thread_worker_threads.parentPort.on('message', (message: any) => mainPeer.handleMessage(thread_v8.deserialize(message)));
44
45
 
45
46
  mainPeer.params.eval = async (script: string, moduleNames: string[], paramNames: string[], ...paramValues: any[]) => {
46
- const f = vm.compileFunction(`return (${script})`, paramNames, {
47
+ const f = thread_vm.compileFunction(`return (${script})`, paramNames, {
47
48
  filename: 'script.js',
48
49
  });
49
50
  const params: any = {};
@@ -92,31 +93,3 @@ export async function newThread<T>(...args: any[]): Promise<T> {
92
93
  worker.terminate();
93
94
  }
94
95
  }
95
-
96
- async function test() {
97
- const foo = 5;
98
- const bar = 6;
99
-
100
- console.log(await newThread({
101
- foo, bar,
102
- }, async () => {
103
- return foo + bar;
104
- }));
105
-
106
-
107
- console.log(await newThread({
108
- foo, bar,
109
- }, async ({foo,bar}) => {
110
- return foo + bar;
111
- }));
112
-
113
- const sayHelloInMainThread = () => console.log('hello! main thread:', worker_threads.isMainThread);
114
- await newThread({
115
- sayHelloInMainThread,
116
- }, async () => {
117
- sayHelloInMainThread();
118
- })
119
- }
120
-
121
- // if (true)
122
- // test();
@@ -0,0 +1,30 @@
1
+ import net from 'net';
2
+ import { listenZeroSingleClient } from "../src/listen-zero";
3
+ import { createDuplexRpcPeer } from "../src/rpc-serializer";
4
+
5
+ async function test() {
6
+ const { port, clientPromise } = await listenZeroSingleClient();
7
+
8
+
9
+ const n1 = net.connect({
10
+ port,
11
+ host: '127.0.0.1',
12
+ });
13
+
14
+ const n2 = await clientPromise;
15
+ console.log('connected');
16
+
17
+ const p1 = createDuplexRpcPeer('p1', 'p2', n1, n1);
18
+ const p2 = createDuplexRpcPeer('p2', 'p1', n2, n2);
19
+
20
+ p1.params.test = () => console.log('p1 test');
21
+ p2.params.test = () => console.log('p2 test');
22
+
23
+ await (await p1.getParam('test'))();
24
+ await (await p2.getParam('test'))();
25
+
26
+ n1.destroy();
27
+ n2.destroy();
28
+ }
29
+
30
+ test();
@@ -0,0 +1,29 @@
1
+ import worker_threads from 'worker_threads';
2
+ import { newThread } from "../src/threading";
3
+
4
+ async function test() {
5
+ const foo = 5;
6
+ const bar = 6;
7
+
8
+ console.log(await newThread({
9
+ foo, bar,
10
+ }, async () => {
11
+ return foo + bar;
12
+ }));
13
+
14
+
15
+ console.log(await newThread({
16
+ foo, bar,
17
+ }, async ({ foo, bar }) => {
18
+ return foo + bar;
19
+ }));
20
+
21
+ const sayHelloInMainThread = () => console.log('hello! main thread:', worker_threads.isMainThread);
22
+ await newThread({
23
+ sayHelloInMainThread,
24
+ }, async () => {
25
+ sayHelloInMainThread();
26
+ })
27
+ }
28
+
29
+ test();
@@ -1,23 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.listenZeroExpress = exports.listenZero = void 0;
4
- const events_1 = require("events");
5
- async function listenZero(server) {
6
- server.listen(0);
7
- await (0, events_1.once)(server, 'listening');
8
- return server.address().port;
9
- }
10
- exports.listenZero = listenZero;
11
- function listenZeroExpress(app) {
12
- const server = app.listen(0);
13
- return {
14
- server,
15
- port: (async () => {
16
- await (0, events_1.once)(server, 'listening');
17
- const { port } = server.address();
18
- return port;
19
- })()
20
- };
21
- }
22
- exports.listenZeroExpress = listenZeroExpress;
23
- //# sourceMappingURL=listen-zero.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"listen-zero.js","sourceRoot":"","sources":["../../src/plugin/listen-zero.ts"],"names":[],"mappings":";;;AACA,mCAA8B;AAGvB,KAAK,UAAU,UAAU,CAAC,MAAkB;IAC/C,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACjB,MAAM,IAAA,aAAI,EAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAChC,OAAQ,MAAM,CAAC,OAAO,EAAsB,CAAC,IAAI,CAAC;AACtD,CAAC;AAJD,gCAIC;AAED,SAAgB,iBAAiB,CAAC,GAAoB;IAClD,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAC7B,OAAO;QACH,MAAM;QACN,IAAI,EAAE,CAAC,KAAK,IAAI,EAAE;YACd,MAAM,IAAA,aAAI,EAAC,MAAM,EAAE,WAAW,CAAC,CAAC;YAChC,MAAM,EAAE,IAAI,EAAE,GAAI,MAAM,CAAC,OAAO,EAAsB,CAAC;YACvD,OAAO,IAAI,CAAC;QAChB,CAAC,CAAC,EAAE;KACP,CAAA;AACL,CAAC;AAVD,8CAUC"}
@@ -1,21 +0,0 @@
1
- import net from 'net';
2
- import { once } from 'events';
3
- import express from 'express';
4
-
5
- export async function listenZero(server: net.Server) {
6
- server.listen(0);
7
- await once(server, 'listening');
8
- return (server.address() as net.AddressInfo).port;
9
- }
10
-
11
- export function listenZeroExpress(app: express.Express) {
12
- const server = app.listen(0);
13
- return {
14
- server,
15
- port: (async () => {
16
- await once(server, 'listening');
17
- const { port } = (server.address() as net.AddressInfo);
18
- return port;
19
- })()
20
- }
21
- }