@scrypted/server 0.123.34 → 0.123.35
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.
- package/dist/cluster/cluster-labels.d.ts +5 -0
- package/dist/cluster/cluster-labels.js +15 -5
- package/dist/cluster/cluster-labels.js.map +1 -1
- package/dist/cluster/cluster-setup.js +9 -2
- package/dist/cluster/cluster-setup.js.map +1 -1
- package/dist/plugin/plugin-host.js +2 -1
- package/dist/plugin/plugin-host.js.map +1 -1
- package/dist/plugin/plugin-remote-worker.js +2 -2
- package/dist/plugin/plugin-remote-worker.js.map +1 -1
- package/dist/scrypted-cluster-main.d.ts +4 -1
- package/dist/scrypted-cluster-main.js +94 -77
- package/dist/scrypted-cluster-main.js.map +1 -1
- package/dist/scrypted-server-main.js +6 -6
- package/dist/scrypted-server-main.js.map +1 -1
- package/dist/services/cluster-fork.js +19 -9
- package/dist/services/cluster-fork.js.map +1 -1
- package/package.json +1 -1
- package/python/cluster_labels.py +4 -1
- package/python/cluster_setup.py +16 -7
- package/python/plugin_console.py +1 -0
- package/python/plugin_pip.py +14 -8
- package/python/plugin_remote.py +120 -38
- package/python/plugin_repl.py +42 -15
- package/python/plugin_volume.py +17 -11
- package/python/rpc-iterator-test.py +11 -8
- package/python/rpc.py +242 -154
- package/python/rpc_reader.py +35 -28
- package/src/cluster/cluster-labels.ts +16 -5
- package/src/cluster/cluster-setup.ts +9 -2
- package/src/plugin/plugin-host.ts +4 -2
- package/src/plugin/plugin-remote-worker.ts +4 -5
- package/src/scrypted-cluster-main.ts +110 -89
- package/src/scrypted-server-main.ts +9 -8
- package/src/services/cluster-fork.ts +25 -11
package/python/rpc_reader.py
CHANGED
@@ -16,7 +16,7 @@ import json
|
|
16
16
|
|
17
17
|
class BufferSerializer(rpc.RpcSerializer):
|
18
18
|
def serialize(self, value, serializationContext):
|
19
|
-
return base64.b64encode(value).decode(
|
19
|
+
return base64.b64encode(value).decode("utf8")
|
20
20
|
|
21
21
|
def deserialize(self, value, serializationContext):
|
22
22
|
return base64.b64decode(value)
|
@@ -24,15 +24,15 @@ class BufferSerializer(rpc.RpcSerializer):
|
|
24
24
|
|
25
25
|
class SidebandBufferSerializer(rpc.RpcSerializer):
|
26
26
|
def serialize(self, value, serializationContext):
|
27
|
-
buffers = serializationContext.get(
|
27
|
+
buffers = serializationContext.get("buffers", None)
|
28
28
|
if not buffers:
|
29
29
|
buffers = []
|
30
|
-
serializationContext[
|
30
|
+
serializationContext["buffers"] = buffers
|
31
31
|
buffers.append(value)
|
32
32
|
return len(buffers) - 1
|
33
33
|
|
34
34
|
def deserialize(self, value, serializationContext):
|
35
|
-
buffers: List = serializationContext.get(
|
35
|
+
buffers: List = serializationContext.get("buffers", None)
|
36
36
|
buffer = buffers.pop()
|
37
37
|
return buffer
|
38
38
|
|
@@ -56,7 +56,7 @@ class RpcFileTransport(RpcTransport):
|
|
56
56
|
super().__init__()
|
57
57
|
self.readFd = readFd
|
58
58
|
self.writeFd = writeFd
|
59
|
-
self.executor = ThreadPoolExecutor(1,
|
59
|
+
self.executor = ThreadPoolExecutor(1, "rpc-read")
|
60
60
|
|
61
61
|
def osReadExact(self, size: int):
|
62
62
|
b = bytes(0)
|
@@ -64,7 +64,7 @@ class RpcFileTransport(RpcTransport):
|
|
64
64
|
got = os.read(self.readFd, size)
|
65
65
|
if not len(got):
|
66
66
|
self.executor.shutdown(False)
|
67
|
-
raise Exception(
|
67
|
+
raise Exception("rpc end of stream reached")
|
68
68
|
size -= len(got)
|
69
69
|
b += got
|
70
70
|
return b
|
@@ -73,7 +73,7 @@ class RpcFileTransport(RpcTransport):
|
|
73
73
|
lengthBytes = self.osReadExact(4)
|
74
74
|
typeBytes = self.osReadExact(1)
|
75
75
|
type = typeBytes[0]
|
76
|
-
length = int.from_bytes(lengthBytes,
|
76
|
+
length = int.from_bytes(lengthBytes, "big")
|
77
77
|
data = self.osReadExact(length - 1)
|
78
78
|
if type == 1:
|
79
79
|
return data
|
@@ -81,11 +81,13 @@ class RpcFileTransport(RpcTransport):
|
|
81
81
|
return message
|
82
82
|
|
83
83
|
async def read(self):
|
84
|
-
return await asyncio.get_event_loop().run_in_executor(
|
84
|
+
return await asyncio.get_event_loop().run_in_executor(
|
85
|
+
self.executor, lambda: self.readMessageInternal()
|
86
|
+
)
|
85
87
|
|
86
88
|
def writeMessage(self, type: int, buffer, reject):
|
87
89
|
length = len(buffer) + 1
|
88
|
-
lb = length.to_bytes(4,
|
90
|
+
lb = length.to_bytes(4, "big")
|
89
91
|
try:
|
90
92
|
for b in [lb, bytes([type]), buffer]:
|
91
93
|
os.write(self.writeFd, b)
|
@@ -94,14 +96,18 @@ class RpcFileTransport(RpcTransport):
|
|
94
96
|
reject(e)
|
95
97
|
|
96
98
|
def writeJSON(self, j, reject):
|
97
|
-
return self.writeMessage(
|
99
|
+
return self.writeMessage(
|
100
|
+
0, bytes(json.dumps(j, allow_nan=False), "utf8"), reject
|
101
|
+
)
|
98
102
|
|
99
103
|
def writeBuffer(self, buffer, reject):
|
100
104
|
return self.writeMessage(1, buffer, reject)
|
101
105
|
|
102
106
|
|
103
107
|
class RpcStreamTransport(RpcTransport):
|
104
|
-
def __init__(
|
108
|
+
def __init__(
|
109
|
+
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
110
|
+
) -> None:
|
105
111
|
super().__init__()
|
106
112
|
self.reader = reader
|
107
113
|
self.writer = writer
|
@@ -110,7 +116,7 @@ class RpcStreamTransport(RpcTransport):
|
|
110
116
|
lengthBytes = await self.reader.readexactly(4)
|
111
117
|
typeBytes = await self.reader.readexactly(1)
|
112
118
|
type = typeBytes[0]
|
113
|
-
length = int.from_bytes(lengthBytes,
|
119
|
+
length = int.from_bytes(lengthBytes, "big")
|
114
120
|
data = await self.reader.readexactly(length - 1)
|
115
121
|
if type == 1:
|
116
122
|
return data
|
@@ -119,7 +125,7 @@ class RpcStreamTransport(RpcTransport):
|
|
119
125
|
|
120
126
|
def writeMessage(self, type: int, buffer, reject):
|
121
127
|
length = len(buffer) + 1
|
122
|
-
lb = length.to_bytes(4,
|
128
|
+
lb = length.to_bytes(4, "big")
|
123
129
|
try:
|
124
130
|
for b in [lb, bytes([type]), buffer]:
|
125
131
|
self.writer.write(b)
|
@@ -128,7 +134,9 @@ class RpcStreamTransport(RpcTransport):
|
|
128
134
|
reject(e)
|
129
135
|
|
130
136
|
def writeJSON(self, j, reject):
|
131
|
-
return self.writeMessage(
|
137
|
+
return self.writeMessage(
|
138
|
+
0, bytes(json.dumps(j, allow_nan=False), "utf8"), reject
|
139
|
+
)
|
132
140
|
|
133
141
|
def writeBuffer(self, buffer, reject):
|
134
142
|
return self.writeMessage(1, buffer, reject)
|
@@ -141,7 +149,9 @@ class RpcConnectionTransport(RpcTransport):
|
|
141
149
|
self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
|
142
150
|
|
143
151
|
async def read(self):
|
144
|
-
return await asyncio.get_event_loop().run_in_executor(
|
152
|
+
return await asyncio.get_event_loop().run_in_executor(
|
153
|
+
self.executor, lambda: self.connection.recv()
|
154
|
+
)
|
145
155
|
|
146
156
|
def writeMessage(self, json, reject):
|
147
157
|
try:
|
@@ -158,23 +168,20 @@ class RpcConnectionTransport(RpcTransport):
|
|
158
168
|
|
159
169
|
|
160
170
|
async def readLoop(loop, peer: rpc.RpcPeer, rpcTransport: RpcTransport):
|
161
|
-
deserializationContext = {
|
162
|
-
'buffers': []
|
163
|
-
}
|
171
|
+
deserializationContext = {"buffers": []}
|
164
172
|
|
165
173
|
while True:
|
166
174
|
message = await rpcTransport.read()
|
167
175
|
|
168
176
|
if type(message) != dict:
|
169
|
-
deserializationContext[
|
177
|
+
deserializationContext["buffers"].append(message)
|
170
178
|
continue
|
171
179
|
|
172
180
|
asyncio.run_coroutine_threadsafe(
|
173
|
-
peer.handleMessage(message, deserializationContext), loop
|
181
|
+
peer.handleMessage(message, deserializationContext), loop
|
182
|
+
)
|
174
183
|
|
175
|
-
deserializationContext = {
|
176
|
-
'buffers': []
|
177
|
-
}
|
184
|
+
deserializationContext = {"buffers": []}
|
178
185
|
|
179
186
|
|
180
187
|
async def prepare_peer_readloop(loop: AbstractEventLoop, rpcTransport: RpcTransport):
|
@@ -185,7 +192,7 @@ async def prepare_peer_readloop(loop: AbstractEventLoop, rpcTransport: RpcTransp
|
|
185
192
|
def send(message, reject=None, serializationContext=None):
|
186
193
|
with mutex:
|
187
194
|
if serializationContext:
|
188
|
-
buffers = serializationContext.get(
|
195
|
+
buffers = serializationContext.get("buffers", None)
|
189
196
|
if buffers:
|
190
197
|
for buffer in buffers:
|
191
198
|
rpcTransport.writeBuffer(buffer, reject)
|
@@ -193,10 +200,10 @@ async def prepare_peer_readloop(loop: AbstractEventLoop, rpcTransport: RpcTransp
|
|
193
200
|
rpcTransport.writeJSON(message, reject)
|
194
201
|
|
195
202
|
peer = rpc.RpcPeer(send)
|
196
|
-
peer.nameDeserializerMap[
|
197
|
-
peer.constructorSerializerMap[bytes] =
|
198
|
-
peer.constructorSerializerMap[bytearray] =
|
199
|
-
peer.constructorSerializerMap[memoryview] =
|
203
|
+
peer.nameDeserializerMap["Buffer"] = SidebandBufferSerializer()
|
204
|
+
peer.constructorSerializerMap[bytes] = "Buffer"
|
205
|
+
peer.constructorSerializerMap[bytearray] = "Buffer"
|
206
|
+
peer.constructorSerializerMap[memoryview] = "Buffer"
|
200
207
|
|
201
208
|
async def peerReadLoop():
|
202
209
|
try:
|
@@ -9,13 +9,19 @@ export function matchesClusterLabels(options: ClusterForkOptions, labels: string
|
|
9
9
|
}
|
10
10
|
|
11
11
|
// if there is nothing in the any list, consider it matched
|
12
|
-
let foundAny
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
12
|
+
let foundAny: boolean;
|
13
|
+
if (options?.labels?.any?.length) {
|
14
|
+
for (const label of options.labels.any) {
|
15
|
+
if (labels.includes(label)) {
|
16
|
+
foundAny = true;
|
17
|
+
break;
|
18
|
+
}
|
17
19
|
}
|
18
20
|
}
|
21
|
+
else {
|
22
|
+
foundAny = true;
|
23
|
+
}
|
24
|
+
|
19
25
|
if (!foundAny)
|
20
26
|
return 0;
|
21
27
|
|
@@ -40,3 +46,8 @@ export function needsClusterForkWorker(options: ClusterForkOptions) {
|
|
40
46
|
&& options
|
41
47
|
&& (!matchesClusterLabels(options, getClusterLabels()) || options.clusterWorkerId);
|
42
48
|
}
|
49
|
+
|
50
|
+
export function utilizesClusterForkWorker(options: ClusterForkOptions) {
|
51
|
+
return process.env.SCRYPTED_CLUSTER_ADDRESS
|
52
|
+
&& (options?.labels || options?.clusterWorkerId);
|
53
|
+
}
|
@@ -249,8 +249,15 @@ export function setupCluster(peer: RpcPeer) {
|
|
249
249
|
if (address === SCRYPTED_CLUSTER_ADDRESS && proxyId.startsWith('n-')) {
|
250
250
|
const parts = proxyId.split('-');
|
251
251
|
const pid = parseInt(parts[1]);
|
252
|
-
|
253
|
-
|
252
|
+
const tid = parseInt(parts[2]);
|
253
|
+
if (pid === process.pid) {
|
254
|
+
if (worker_threads.isMainThread && tid === worker_threads.threadId) {
|
255
|
+
// main thread can't call itself, so this may be a different thread cluster.
|
256
|
+
}
|
257
|
+
else {
|
258
|
+
return connectIPCObject(clusterObject, parseInt(parts[2]));
|
259
|
+
}
|
260
|
+
}
|
254
261
|
}
|
255
262
|
|
256
263
|
try {
|
@@ -4,7 +4,7 @@ import * as io from 'engine.io';
|
|
4
4
|
import fs from 'fs';
|
5
5
|
import os from 'os';
|
6
6
|
import WebSocket from 'ws';
|
7
|
-
import {
|
7
|
+
import { utilizesClusterForkWorker } from '../cluster/cluster-labels';
|
8
8
|
import { setupCluster } from '../cluster/cluster-setup';
|
9
9
|
import { Plugin } from '../db-types';
|
10
10
|
import { IOServer, IOServerSocket } from '../io';
|
@@ -351,7 +351,9 @@ export class PluginHost {
|
|
351
351
|
zipFile: this.zipFile,
|
352
352
|
zipHash: this.zipHash,
|
353
353
|
};
|
354
|
-
|
354
|
+
|
355
|
+
// if a plugin requests a cluster worker, and it can be fulfilled by the server, do it.
|
356
|
+
if (!utilizesClusterForkWorker(this.packageJson.scrypted)) {
|
355
357
|
this.peer = new RpcPeer('host', this.pluginId, (message, reject, serializationContext) => {
|
356
358
|
if (connected) {
|
357
359
|
this.worker.send(message, reject, serializationContext);
|
@@ -4,10 +4,12 @@ import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
5
5
|
import { install as installSourceMapSupport } from 'source-map-support';
|
6
6
|
import worker_threads from 'worker_threads';
|
7
|
-
import {
|
7
|
+
import { utilizesClusterForkWorker } from '../cluster/cluster-labels';
|
8
8
|
import { setupCluster } from '../cluster/cluster-setup';
|
9
9
|
import { RpcMessage, RpcPeer } from '../rpc';
|
10
10
|
import { evalLocal } from '../rpc-peer-eval';
|
11
|
+
import { ClusterManagerImpl } from '../scrypted-cluster-main';
|
12
|
+
import type { PluginComponent } from '../services/plugin';
|
11
13
|
import type { DeviceManagerImpl } from './device';
|
12
14
|
import { MediaManagerImpl } from './media';
|
13
15
|
import { PluginAPI, PluginAPIProxy, PluginRemote, PluginRemoteLoadZipOptions, PluginZipAPI } from './plugin-api';
|
@@ -22,9 +24,6 @@ import { NodeThreadWorker } from './runtime/node-thread-worker';
|
|
22
24
|
import { prepareZip } from './runtime/node-worker-common';
|
23
25
|
import { getBuiltinRuntimeHosts } from './runtime/runtime-host';
|
24
26
|
import { RuntimeWorker, RuntimeWorkerOptions } from './runtime/runtime-worker';
|
25
|
-
import type { ClusterForkService } from '../services/cluster-fork';
|
26
|
-
import type { PluginComponent } from '../services/plugin';
|
27
|
-
import { ClusterManagerImpl } from '../scrypted-cluster-main';
|
28
27
|
|
29
28
|
const serverVersion = require('../../package.json').version;
|
30
29
|
|
@@ -226,7 +225,7 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
|
|
226
225
|
};
|
227
226
|
|
228
227
|
// if running in a cluster, fork to a matching cluster worker only if necessary.
|
229
|
-
if (
|
228
|
+
if (utilizesClusterForkWorker(options)) {
|
230
229
|
({ runtimeWorker, forkPeer, clusterWorkerId } = createClusterForkWorker(
|
231
230
|
runtimeWorkerOptions,
|
232
231
|
options,
|
@@ -74,7 +74,9 @@ export interface ClusterWorkerProperties {
|
|
74
74
|
|
75
75
|
export interface RunningClusterWorker extends ClusterWorkerProperties {
|
76
76
|
id: string;
|
77
|
+
name: string;
|
77
78
|
peer: RpcPeer;
|
79
|
+
fork: Promise<ClusterForkParam>;
|
78
80
|
forks: Set<ClusterForkOptions>;
|
79
81
|
address: string;
|
80
82
|
}
|
@@ -113,6 +115,88 @@ export interface ClusterForkResultInterface {
|
|
113
115
|
|
114
116
|
export type ClusterForkParam = (runtime: string, options: RuntimeWorkerOptions, peerLiveness: PeerLiveness, getZip: () => Promise<Buffer>) => Promise<ClusterForkResultInterface>;
|
115
117
|
|
118
|
+
function createClusterForkParam(mainFilename: string, clusterId: string, clusterSecret: string) {
|
119
|
+
const clusterForkParam: ClusterForkParam = async (runtime, runtimeWorkerOptions, peerLiveness, getZip) => {
|
120
|
+
let runtimeWorker: RuntimeWorker;
|
121
|
+
|
122
|
+
const builtins = getBuiltinRuntimeHosts();
|
123
|
+
const rt = builtins.get(runtime);
|
124
|
+
if (!rt)
|
125
|
+
throw new Error('unknown runtime ' + runtime);
|
126
|
+
|
127
|
+
const pluginId: string = runtimeWorkerOptions.packageJson.name;
|
128
|
+
const { zipFile, unzippedPath } = await prepareZip(getPluginVolume(pluginId), runtimeWorkerOptions.zipHash, getZip);
|
129
|
+
|
130
|
+
const volume = getScryptedVolume();
|
131
|
+
const pluginVolume = getPluginVolume(pluginId);
|
132
|
+
|
133
|
+
runtimeWorkerOptions.zipFile = zipFile;
|
134
|
+
runtimeWorkerOptions.unzippedPath = unzippedPath;
|
135
|
+
|
136
|
+
runtimeWorkerOptions.env = {
|
137
|
+
...runtimeWorkerOptions.env,
|
138
|
+
SCRYPTED_VOLUME: volume,
|
139
|
+
SCRYPTED_PLUGIN_VOLUME: pluginVolume,
|
140
|
+
};
|
141
|
+
|
142
|
+
runtimeWorker = rt(mainFilename, runtimeWorkerOptions, undefined);
|
143
|
+
runtimeWorker.stdout.on('data', data => console.log(data.toString()));
|
144
|
+
runtimeWorker.stderr.on('data', data => console.error(data.toString()));
|
145
|
+
|
146
|
+
const threadPeer = new RpcPeer('main', 'thread', (message, reject, serializationContext) => runtimeWorker.send(message, reject, serializationContext));
|
147
|
+
runtimeWorker.setupRpcPeer(threadPeer);
|
148
|
+
runtimeWorker.on('exit', () => {
|
149
|
+
threadPeer.kill('worker exited');
|
150
|
+
});
|
151
|
+
runtimeWorker.on('error', e => {
|
152
|
+
threadPeer.kill('worker error ' + e);
|
153
|
+
});
|
154
|
+
threadPeer.killedSafe.finally(() => {
|
155
|
+
runtimeWorker.kill();
|
156
|
+
});
|
157
|
+
peerLiveness.waitKilled().catch(() => { }).finally(() => {
|
158
|
+
threadPeer.kill('peer killed');
|
159
|
+
});
|
160
|
+
let getRemote: any;
|
161
|
+
let ping: any;
|
162
|
+
try {
|
163
|
+
const initializeCluster: InitializeCluster = await threadPeer.getParam('initializeCluster');
|
164
|
+
await initializeCluster({ clusterId, clusterSecret });
|
165
|
+
getRemote = await threadPeer.getParam('getRemote');
|
166
|
+
ping = await threadPeer.getParam('ping');
|
167
|
+
}
|
168
|
+
catch (e) {
|
169
|
+
threadPeer.kill('cluster fork failed');
|
170
|
+
throw e;
|
171
|
+
}
|
172
|
+
|
173
|
+
const readStream = async function* (stream: Readable) {
|
174
|
+
for await (const buffer of stream) {
|
175
|
+
yield buffer;
|
176
|
+
}
|
177
|
+
}
|
178
|
+
|
179
|
+
const timeout = setTimeout(() => {
|
180
|
+
threadPeer.kill('cluster fork timeout');
|
181
|
+
}, 10000);
|
182
|
+
const clusterGetRemote = (...args: any[]) => {
|
183
|
+
clearTimeout(timeout);
|
184
|
+
return {
|
185
|
+
[RpcPeer.PROPERTY_JSON_COPY_SERIALIZE_CHILDREN]: true,
|
186
|
+
stdout: readStream(runtimeWorker.stdout),
|
187
|
+
stderr: readStream(runtimeWorker.stderr),
|
188
|
+
getRemote,
|
189
|
+
ping,
|
190
|
+
};
|
191
|
+
};
|
192
|
+
|
193
|
+
const result = new ClusterForkResult(threadPeer, threadPeer.killed, clusterGetRemote);
|
194
|
+
return result;
|
195
|
+
};
|
196
|
+
|
197
|
+
return clusterForkParam;
|
198
|
+
}
|
199
|
+
|
116
200
|
export function startClusterClient(mainFilename: string) {
|
117
201
|
console.log('Cluster client starting.');
|
118
202
|
const originalClusterAddress = process.env.SCRYPTED_CLUSTER_ADDRESS;
|
@@ -174,7 +258,7 @@ export function startClusterClient(mainFilename: string) {
|
|
174
258
|
const auth: ClusterObject = {
|
175
259
|
address: socket.localAddress,
|
176
260
|
port: socket.localPort,
|
177
|
-
id: process.env.
|
261
|
+
id: process.env.SCRYPTED_CLUSTER_WORKER_NAME || os.hostname(),
|
178
262
|
proxyId: undefined,
|
179
263
|
sourceKey: undefined,
|
180
264
|
sha256: undefined,
|
@@ -190,85 +274,7 @@ export function startClusterClient(mainFilename: string) {
|
|
190
274
|
const clusterPeerSetup = setupCluster(peer);
|
191
275
|
await clusterPeerSetup.initializeCluster({ clusterId, clusterSecret });
|
192
276
|
|
193
|
-
|
194
|
-
let runtimeWorker: RuntimeWorker;
|
195
|
-
|
196
|
-
const builtins = getBuiltinRuntimeHosts();
|
197
|
-
const rt = builtins.get(runtime);
|
198
|
-
if (!rt)
|
199
|
-
throw new Error('unknown runtime ' + runtime);
|
200
|
-
|
201
|
-
const pluginId: string = runtimeWorkerOptions.packageJson.name;
|
202
|
-
const { zipFile, unzippedPath } = await prepareZip(getPluginVolume(pluginId), runtimeWorkerOptions.zipHash, getZip);
|
203
|
-
|
204
|
-
const volume = getScryptedVolume();
|
205
|
-
const pluginVolume = getPluginVolume(pluginId);
|
206
|
-
|
207
|
-
runtimeWorkerOptions.zipFile = zipFile;
|
208
|
-
runtimeWorkerOptions.unzippedPath = unzippedPath;
|
209
|
-
|
210
|
-
runtimeWorkerOptions.env = {
|
211
|
-
...runtimeWorkerOptions.env,
|
212
|
-
SCRYPTED_VOLUME: volume,
|
213
|
-
SCRYPTED_PLUGIN_VOLUME: pluginVolume,
|
214
|
-
};
|
215
|
-
|
216
|
-
runtimeWorker = rt(mainFilename, runtimeWorkerOptions, undefined);
|
217
|
-
runtimeWorker.stdout.on('data', data => console.log(data.toString()));
|
218
|
-
runtimeWorker.stderr.on('data', data => console.error(data.toString()));
|
219
|
-
|
220
|
-
const threadPeer = new RpcPeer('main', 'thread', (message, reject, serializationContext) => runtimeWorker.send(message, reject, serializationContext));
|
221
|
-
runtimeWorker.setupRpcPeer(threadPeer);
|
222
|
-
runtimeWorker.on('exit', () => {
|
223
|
-
threadPeer.kill('worker exited');
|
224
|
-
});
|
225
|
-
runtimeWorker.on('error', e => {
|
226
|
-
threadPeer.kill('worker error ' + e);
|
227
|
-
});
|
228
|
-
threadPeer.killedSafe.finally(() => {
|
229
|
-
runtimeWorker.kill();
|
230
|
-
});
|
231
|
-
peerLiveness.waitKilled().catch(() => { }).finally(() => {
|
232
|
-
threadPeer.kill('peer killed');
|
233
|
-
});
|
234
|
-
let getRemote: any;
|
235
|
-
let ping: any;
|
236
|
-
try {
|
237
|
-
const initializeCluster: InitializeCluster = await threadPeer.getParam('initializeCluster');
|
238
|
-
await initializeCluster({ clusterId, clusterSecret });
|
239
|
-
getRemote = await threadPeer.getParam('getRemote');
|
240
|
-
ping = await threadPeer.getParam('ping');
|
241
|
-
}
|
242
|
-
catch (e) {
|
243
|
-
threadPeer.kill('cluster fork failed');
|
244
|
-
throw e;
|
245
|
-
}
|
246
|
-
|
247
|
-
const readStream = async function* (stream: Readable) {
|
248
|
-
for await (const buffer of stream) {
|
249
|
-
yield buffer;
|
250
|
-
}
|
251
|
-
}
|
252
|
-
|
253
|
-
const timeout = setTimeout(() => {
|
254
|
-
threadPeer.kill('cluster fork timeout');
|
255
|
-
}, 10000);
|
256
|
-
const clusterGetRemote = (...args: any[]) => {
|
257
|
-
clearTimeout(timeout);
|
258
|
-
return {
|
259
|
-
[RpcPeer.PROPERTY_JSON_COPY_SERIALIZE_CHILDREN]: true,
|
260
|
-
stdout: readStream(runtimeWorker.stdout),
|
261
|
-
stderr: readStream(runtimeWorker.stderr),
|
262
|
-
getRemote,
|
263
|
-
ping,
|
264
|
-
};
|
265
|
-
};
|
266
|
-
|
267
|
-
const result = new ClusterForkResult(threadPeer, threadPeer.killed, clusterGetRemote);
|
268
|
-
return result;
|
269
|
-
};
|
270
|
-
|
271
|
-
peer.params['fork'] = clusterForkParam;
|
277
|
+
peer.params['fork'] = createClusterForkParam(mainFilename, clusterId, clusterSecret);
|
272
278
|
|
273
279
|
await peer.killed;
|
274
280
|
}
|
@@ -284,7 +290,20 @@ export function startClusterClient(mainFilename: string) {
|
|
284
290
|
})();
|
285
291
|
}
|
286
292
|
|
287
|
-
export function createClusterServer(
|
293
|
+
export function createClusterServer(mainFilename: string, scryptedRuntime: ScryptedRuntime, certificate: ReturnType<typeof createSelfSignedCertificate>) {
|
294
|
+
const serverClusterWorkerId = crypto.randomUUID();
|
295
|
+
process.env.SCRYPTED_CLUSTER_WORKER_ID = serverClusterWorkerId;
|
296
|
+
const serverWorker: RunningClusterWorker = {
|
297
|
+
labels: getClusterLabels(),
|
298
|
+
id: serverClusterWorkerId,
|
299
|
+
peer: undefined,
|
300
|
+
fork: Promise.resolve(createClusterForkParam(mainFilename, scryptedRuntime.clusterId, scryptedRuntime.clusterSecret)),
|
301
|
+
name: process.env.SCRYPTED_CLUSTER_WORKER_NAME || os.hostname(),
|
302
|
+
address: process.env.SCRYPTED_CLUSTER_ADDRESS,
|
303
|
+
forks: new Set(),
|
304
|
+
};
|
305
|
+
scryptedRuntime.clusterWorkers.set(serverClusterWorkerId, serverWorker);
|
306
|
+
|
288
307
|
const server = tls.createServer({
|
289
308
|
key: certificate.serviceKey,
|
290
309
|
cert: certificate.certificate,
|
@@ -299,7 +318,7 @@ export function createClusterServer(runtime: ScryptedRuntime, certificate: Retur
|
|
299
318
|
const connectForkWorker: ConnectForkWorker = async (auth: ClusterObject, properties: ClusterWorkerProperties) => {
|
300
319
|
const id = crypto.randomUUID();
|
301
320
|
try {
|
302
|
-
const sha256 = computeClusterObjectHash(auth,
|
321
|
+
const sha256 = computeClusterObjectHash(auth, scryptedRuntime.clusterSecret);
|
303
322
|
if (sha256 !== auth.sha256)
|
304
323
|
throw new Error('cluster object hash mismatch');
|
305
324
|
|
@@ -313,18 +332,19 @@ export function createClusterServer(runtime: ScryptedRuntime, certificate: Retur
|
|
313
332
|
}
|
314
333
|
const worker: RunningClusterWorker = {
|
315
334
|
...properties,
|
316
|
-
// generate a random uuid.
|
317
335
|
id,
|
318
336
|
peer,
|
337
|
+
fork: undefined,
|
338
|
+
name: auth.id,
|
319
339
|
address: socket.remoteAddress,
|
320
340
|
forks: new Set(),
|
321
341
|
};
|
322
|
-
|
342
|
+
scryptedRuntime.clusterWorkers.set(id, worker);
|
323
343
|
peer.killedSafe.finally(() => {
|
324
|
-
|
344
|
+
scryptedRuntime.clusterWorkers.delete(id);
|
325
345
|
});
|
326
346
|
socket.on('close', () => {
|
327
|
-
|
347
|
+
scryptedRuntime.clusterWorkers.delete(id);
|
328
348
|
});
|
329
349
|
console.log('Cluster client authenticated.', socket.remoteAddress, socket.remotePort, properties);
|
330
350
|
}
|
@@ -334,7 +354,7 @@ export function createClusterServer(runtime: ScryptedRuntime, certificate: Retur
|
|
334
354
|
}
|
335
355
|
|
336
356
|
return {
|
337
|
-
clusterId:
|
357
|
+
clusterId: scryptedRuntime.clusterId,
|
338
358
|
clusterWorkerId: id,
|
339
359
|
}
|
340
360
|
}
|
@@ -345,7 +365,8 @@ export function createClusterServer(runtime: ScryptedRuntime, certificate: Retur
|
|
345
365
|
}
|
346
366
|
|
347
367
|
export class ClusterManagerImpl implements ClusterManager {
|
348
|
-
private
|
368
|
+
private clusterServicePromise: Promise<ClusterForkService>;
|
369
|
+
private clusterMode = getScryptedClusterMode()[0];
|
349
370
|
|
350
371
|
constructor(private api: PluginAPI) {
|
351
372
|
}
|
@@ -355,7 +376,7 @@ export class ClusterManagerImpl implements ClusterManager {
|
|
355
376
|
}
|
356
377
|
|
357
378
|
getClusterMode(): 'server' | 'client' | undefined {
|
358
|
-
return
|
379
|
+
return this.clusterMode;
|
359
380
|
}
|
360
381
|
|
361
382
|
async getClusterWorkers(): Promise<Record<string, ClusterWorker>> {
|
@@ -23,7 +23,7 @@ import { getNpmPackageInfo } from './services/plugin';
|
|
23
23
|
import { setScryptedUserPassword, UsersService } from './services/users';
|
24
24
|
import { sleep } from './sleep';
|
25
25
|
import { ONE_DAY_MILLISECONDS, UserToken } from './usertoken';
|
26
|
-
import { createClusterServer } from './scrypted-cluster-main';
|
26
|
+
import { createClusterServer, startClusterClient } from './scrypted-cluster-main';
|
27
27
|
import { getScryptedClusterMode } from './cluster/cluster-setup';
|
28
28
|
|
29
29
|
export type Runtime = ScryptedRuntime;
|
@@ -351,6 +351,14 @@ async function start(mainFilename: string, options?: {
|
|
351
351
|
|
352
352
|
const scrypted = new ScryptedRuntime(mainFilename, db, insecure, secure, app);
|
353
353
|
await options?.onRuntimeCreated?.(scrypted);
|
354
|
+
|
355
|
+
const clusterMode = getScryptedClusterMode();
|
356
|
+
if (clusterMode?.[0] === 'server') {
|
357
|
+
console.log('Cluster server starting.');
|
358
|
+
const clusterServer = createClusterServer(mainFilename, scrypted, keyPair);
|
359
|
+
await listenServerPort('SCRYPTED_CLUSTER_SERVER', clusterMode[2], clusterServer);
|
360
|
+
}
|
361
|
+
|
354
362
|
await scrypted.start();
|
355
363
|
|
356
364
|
|
@@ -736,13 +744,6 @@ async function start(mainFilename: string, options?: {
|
|
736
744
|
await listenServerPort('SCRYPTED_SECURE_PORT', SCRYPTED_SECURE_PORT, secure);
|
737
745
|
await listenServerPort('SCRYPTED_INSECURE_PORT', SCRYPTED_INSECURE_PORT, insecure);
|
738
746
|
|
739
|
-
const clusterMode = getScryptedClusterMode();
|
740
|
-
if (clusterMode?.[0] === 'server') {
|
741
|
-
console.log('Cluster server starting.');
|
742
|
-
const clusterServer = createClusterServer(scrypted, keyPair);
|
743
|
-
await listenServerPort('SCRYPTED_CLUSTER_SERVER', clusterMode[2], clusterServer);
|
744
|
-
}
|
745
|
-
|
746
747
|
console.log('#######################################################');
|
747
748
|
console.log(`Scrypted Volume : ${volumeDir}`);
|
748
749
|
console.log(`Scrypted Server (Local) : https://localhost:${SCRYPTED_SECURE_PORT}/`);
|
@@ -14,7 +14,7 @@ class WrappedForkResult implements ClusterForkResultInterface {
|
|
14
14
|
}
|
15
15
|
|
16
16
|
async kill() {
|
17
|
-
const fr = await this.forkResult.catch(() => {});
|
17
|
+
const fr = await this.forkResult.catch(() => { });
|
18
18
|
if (!fr)
|
19
19
|
return;
|
20
20
|
await fr.kill();
|
@@ -35,7 +35,7 @@ export class ClusterForkService {
|
|
35
35
|
constructor(public runtime: ScryptedRuntime) { }
|
36
36
|
|
37
37
|
async fork(runtimeWorkerOptions: RuntimeWorkerOptions, options: ClusterForkOptions, peerLiveness: PeerLiveness, getZip: () => Promise<Buffer>) {
|
38
|
-
|
38
|
+
let matchingWorkers = [...this.runtime.clusterWorkers.entries()].map(([id, worker]) => ({
|
39
39
|
worker,
|
40
40
|
matches: matchesClusterLabels(options, worker.labels),
|
41
41
|
}))
|
@@ -44,7 +44,6 @@ export class ClusterForkService {
|
|
44
44
|
// and worker id must match if provided
|
45
45
|
return matches && (!options.clusterWorkerId || worker.id === options.clusterWorkerId);
|
46
46
|
});
|
47
|
-
matchingWorkers.sort((a, b) => b.worker.labels.length - a.worker.labels.length);
|
48
47
|
|
49
48
|
let worker: RunningClusterWorker;
|
50
49
|
|
@@ -53,16 +52,31 @@ export class ClusterForkService {
|
|
53
52
|
if (options.id)
|
54
53
|
worker = matchingWorkers.find(({ worker }) => [...worker.forks].find(f => f.id === options.id))?.worker;
|
55
54
|
|
56
|
-
// TODO: round robin?
|
57
|
-
worker ||= matchingWorkers[0]?.worker;
|
58
|
-
|
59
55
|
if (!worker) {
|
60
|
-
|
61
|
-
|
62
|
-
|
56
|
+
// sort by number of matches, to find the best match.
|
57
|
+
matchingWorkers.sort((a, b) => b.matches - a.matches);
|
58
|
+
|
59
|
+
const bestMatch = matchingWorkers[0];
|
60
|
+
|
61
|
+
if (!bestMatch) {
|
62
|
+
if (options.clusterWorkerId)
|
63
|
+
throw new Error(`no worker found for cluster id ${options.clusterWorkerId}`);
|
64
|
+
throw new Error(`no worker found for cluster labels ${JSON.stringify(options.labels)}`);
|
65
|
+
}
|
66
|
+
|
67
|
+
// filter out workers that are not equivalent to the best match.
|
68
|
+
// this enforces the "prefer" label.
|
69
|
+
matchingWorkers = matchingWorkers.filter(({ matches }) => matches === bestMatch.matches)
|
70
|
+
// sort by number of forks, to distribute load.
|
71
|
+
.sort((a, b) => a.worker.forks.size - b.worker.forks.size);
|
72
|
+
|
73
|
+
worker = matchingWorkers[0]?.worker;
|
63
74
|
}
|
64
75
|
|
65
|
-
|
76
|
+
console.log('forking to worker', worker.id, options);
|
77
|
+
|
78
|
+
worker.fork ||= worker.peer.getParam('fork');
|
79
|
+
const fork: ClusterForkParam = await worker.fork;
|
66
80
|
const forkResultPromise = fork(options.runtime, runtimeWorkerOptions, peerLiveness, getZip);
|
67
81
|
|
68
82
|
options.id ||= this.runtime.findPluginDevice(runtimeWorkerOptions.packageJson.name)?._id;
|
@@ -83,7 +97,7 @@ export class ClusterForkService {
|
|
83
97
|
const ret: any = {};
|
84
98
|
for (const worker of this.runtime.clusterWorkers.values()) {
|
85
99
|
ret[worker.id] = {
|
86
|
-
name: worker.
|
100
|
+
name: worker.name,
|
87
101
|
labels: worker.labels,
|
88
102
|
forks: [...worker.forks],
|
89
103
|
};
|