@scrypted/server 0.123.34 → 0.123.36

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.
@@ -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('utf8')
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('buffers', None)
27
+ buffers = serializationContext.get("buffers", None)
28
28
  if not buffers:
29
29
  buffers = []
30
- serializationContext['buffers'] = buffers
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('buffers', None)
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, 'rpc-read')
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('rpc end of stream reached')
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, 'big')
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(self.executor, lambda: self.readMessageInternal())
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, 'big')
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(0, bytes(json.dumps(j, allow_nan=False), 'utf8'), reject)
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__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
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, 'big')
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, 'big')
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(0, bytes(json.dumps(j, allow_nan=False), 'utf8'), reject)
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(self.executor, lambda: self.connection.recv())
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['buffers'].append(message)
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('buffers', None)
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['Buffer'] = SidebandBufferSerializer()
197
- peer.constructorSerializerMap[bytes] = 'Buffer'
198
- peer.constructorSerializerMap[bytearray] = 'Buffer'
199
- peer.constructorSerializerMap[memoryview] = 'Buffer'
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 = !options?.labels?.any?.length;
13
- for (const label of options.labels?.any || []) {
14
- if (labels.includes(label)) {
15
- matched++;
16
- foundAny = true;
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
- if (pid === process.pid)
253
- return connectIPCObject(clusterObject, parseInt(parts[2]));
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 { needsClusterForkWorker } from '../cluster/cluster-labels';
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
- if (!needsClusterForkWorker(this.packageJson.scrypted)) {
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 { needsClusterForkWorker } from '../cluster/cluster-labels';
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 (needsClusterForkWorker(options)) {
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.SCRYPTED_CLUSTER_CLIENT_NAME || os.hostname(),
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
- const clusterForkParam: ClusterForkParam = async (runtime, runtimeWorkerOptions, peerLiveness, getZip) => {
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(runtime: ScryptedRuntime, certificate: ReturnType<typeof createSelfSignedCertificate>) {
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, runtime.clusterSecret);
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
- runtime.clusterWorkers.set(id, worker);
342
+ scryptedRuntime.clusterWorkers.set(id, worker);
323
343
  peer.killedSafe.finally(() => {
324
- runtime.clusterWorkers.delete(id);
344
+ scryptedRuntime.clusterWorkers.delete(id);
325
345
  });
326
346
  socket.on('close', () => {
327
- runtime.clusterWorkers.delete(id);
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: runtime.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 clusterServicePromise: Promise<ClusterForkService>;
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 getScryptedClusterMode()[0];
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
- const matchingWorkers = [...this.runtime.clusterWorkers.entries()].map(([id, worker]) => ({
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
- if (options.clusterWorkerId)
61
- throw new Error(`no worker found for cluster id ${options.clusterWorkerId}`);
62
- throw new Error(`no worker found for cluster labels ${JSON.stringify(options.labels)}`);
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
- const fork: ClusterForkParam = await worker.peer.getParam('fork');
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.peer.peerName,
100
+ name: worker.name,
87
101
  labels: worker.labels,
88
102
  forks: [...worker.forks],
89
103
  };