@scrypted/server 0.123.1 → 0.123.2
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/ip.d.ts +2 -0
- package/dist/ip.js +5 -4
- package/dist/ip.js.map +1 -1
- package/dist/listen-zero.d.ts +5 -2
- package/dist/listen-zero.js +2 -1
- package/dist/listen-zero.js.map +1 -1
- package/dist/plugin/plugin-api.d.ts +9 -3
- package/dist/plugin/plugin-api.js +10 -1
- package/dist/plugin/plugin-api.js.map +1 -1
- package/dist/plugin/plugin-console.d.ts +2 -1
- package/dist/plugin/plugin-console.js +27 -6
- package/dist/plugin/plugin-console.js.map +1 -1
- package/dist/plugin/plugin-host-api.js +1 -1
- package/dist/plugin/plugin-host-api.js.map +1 -1
- package/dist/plugin/plugin-host.d.ts +4 -7
- package/dist/plugin/plugin-host.js +76 -62
- package/dist/plugin/plugin-host.js.map +1 -1
- package/dist/plugin/plugin-lazy-remote.d.ts +3 -3
- package/dist/plugin/plugin-lazy-remote.js +2 -2
- package/dist/plugin/plugin-lazy-remote.js.map +1 -1
- package/dist/plugin/plugin-remote-stats.d.ts +2 -2
- package/dist/plugin/plugin-remote-stats.js +4 -2
- package/dist/plugin/plugin-remote-stats.js.map +1 -1
- package/dist/plugin/plugin-remote-worker.d.ts +0 -1
- package/dist/plugin/plugin-remote-worker.js +77 -335
- package/dist/plugin/plugin-remote-worker.js.map +1 -1
- package/dist/plugin/plugin-remote.d.ts +3 -3
- package/dist/plugin/plugin-remote.js +2 -2
- package/dist/plugin/plugin-remote.js.map +1 -1
- package/dist/plugin/plugin-repl.js +2 -1
- package/dist/plugin/plugin-repl.js.map +1 -1
- package/dist/plugin/runtime/cluster-fork.worker.d.ts +9 -0
- package/dist/plugin/runtime/cluster-fork.worker.js +73 -0
- package/dist/plugin/runtime/cluster-fork.worker.js.map +1 -0
- package/dist/plugin/runtime/custom-worker.js +2 -2
- package/dist/plugin/runtime/custom-worker.js.map +1 -1
- package/dist/plugin/runtime/node-fork-worker.js +5 -3
- package/dist/plugin/runtime/node-fork-worker.js.map +1 -1
- package/dist/plugin/runtime/python-worker.js +2 -2
- package/dist/plugin/runtime/python-worker.js.map +1 -1
- package/dist/rpc.d.ts +1 -0
- package/dist/rpc.js +3 -2
- package/dist/rpc.js.map +1 -1
- package/dist/runtime.d.ts +4 -0
- package/dist/runtime.js +16 -2
- package/dist/runtime.js.map +1 -1
- package/dist/scrypted-cluster-common.d.ts +22 -0
- package/dist/scrypted-cluster-common.js +332 -0
- package/dist/scrypted-cluster-common.js.map +1 -0
- package/dist/scrypted-cluster-main.d.ts +2 -0
- package/dist/scrypted-cluster-main.js +12 -0
- package/dist/scrypted-cluster-main.js.map +1 -0
- package/dist/scrypted-cluster.d.ts +38 -0
- package/dist/scrypted-cluster.js +277 -0
- package/dist/scrypted-cluster.js.map +1 -0
- package/dist/scrypted-main-exports.js +20 -14
- package/dist/scrypted-main-exports.js.map +1 -1
- package/dist/scrypted-server-main.js +8 -15
- package/dist/scrypted-server-main.js.map +1 -1
- package/dist/server-settings.d.ts +1 -0
- package/dist/server-settings.js +2 -1
- package/dist/server-settings.js.map +1 -1
- package/dist/services/backup.js.map +1 -1
- package/dist/services/cluster-fork.d.ts +7 -0
- package/dist/services/cluster-fork.js +25 -0
- package/dist/services/cluster-fork.js.map +1 -0
- package/dist/services/plugin.d.ts +2 -7
- package/dist/services/plugin.js +2 -17
- package/dist/services/plugin.js.map +1 -1
- package/package.json +2 -2
- package/python/plugin_remote.py +150 -135
- package/python/rpc_reader.py +3 -19
- package/src/ip.ts +5 -4
- package/src/listen-zero.ts +3 -2
- package/src/plugin/plugin-api.ts +11 -3
- package/src/plugin/plugin-console.ts +29 -7
- package/src/plugin/plugin-host-api.ts +1 -1
- package/src/plugin/plugin-host.ts +92 -77
- package/src/plugin/plugin-lazy-remote.ts +4 -4
- package/src/plugin/plugin-remote-stats.ts +6 -4
- package/src/plugin/plugin-remote-worker.ts +91 -376
- package/src/plugin/plugin-remote.ts +5 -5
- package/src/plugin/plugin-repl.ts +2 -1
- package/src/plugin/runtime/cluster-fork.worker.ts +92 -0
- package/src/plugin/runtime/custom-worker.ts +2 -2
- package/src/plugin/runtime/node-fork-worker.ts +6 -3
- package/src/plugin/runtime/python-worker.ts +2 -2
- package/src/rpc.ts +3 -2
- package/src/runtime.ts +17 -2
- package/src/scrypted-cluster-common.ts +374 -0
- package/src/scrypted-cluster-main.ts +12 -0
- package/src/scrypted-cluster.ts +326 -0
- package/src/scrypted-main-exports.ts +22 -16
- package/src/scrypted-server-main.ts +15 -23
- package/src/server-settings.ts +1 -0
- package/src/services/backup.ts +0 -1
- package/src/services/cluster-fork.ts +22 -0
- package/src/services/plugin.ts +3 -21
@@ -0,0 +1,326 @@
|
|
1
|
+
import type { ForkOptions } from '@scrypted/types';
|
2
|
+
import net from 'net';
|
3
|
+
import os from 'os';
|
4
|
+
import type { Readable } from 'stream';
|
5
|
+
import tls from 'tls';
|
6
|
+
import type { createSelfSignedCertificate } from './cert';
|
7
|
+
import { computeClusterObjectHash } from './cluster/cluster-hash';
|
8
|
+
import type { ClusterObject } from './cluster/connect-rpc-object';
|
9
|
+
import { getPluginVolume, getScryptedVolume } from './plugin/plugin-volume';
|
10
|
+
import { prepareZip } from './plugin/runtime/node-worker-common';
|
11
|
+
import { getBuiltinRuntimeHosts } from './plugin/runtime/runtime-host';
|
12
|
+
import { RuntimeWorker } from './plugin/runtime/runtime-worker';
|
13
|
+
import { RpcPeer } from './rpc';
|
14
|
+
import { createRpcDuplexSerializer } from './rpc-serializer';
|
15
|
+
import type { ScryptedRuntime } from './runtime';
|
16
|
+
import { prepareClusterPeer } from './scrypted-cluster-common';
|
17
|
+
import { sleep } from './sleep';
|
18
|
+
|
19
|
+
export class PeerLiveness {
|
20
|
+
__proxy_oneway_methods = ['kill'];
|
21
|
+
constructor(private killed: Promise<any>) {
|
22
|
+
}
|
23
|
+
async waitKilled() {
|
24
|
+
return this.killed;
|
25
|
+
}
|
26
|
+
}
|
27
|
+
|
28
|
+
export class ClusterForkResult extends PeerLiveness {
|
29
|
+
constructor(private peer: RpcPeer, killed: Promise<any>, private result: any) {
|
30
|
+
super(killed);
|
31
|
+
}
|
32
|
+
|
33
|
+
async kill() {
|
34
|
+
this.peer.kill('killed');
|
35
|
+
}
|
36
|
+
|
37
|
+
async getResult() {
|
38
|
+
return this.result;
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
export type ClusterForkParam = (peerLiveness: PeerLiveness, runtime: string, packageJson: any, zipHash: string, getZip: () => Promise<Buffer>) => Promise<ClusterForkResult>;
|
43
|
+
export type InitializeCluster = (cluster: { clusterId: string, clusterSecret: string }) => Promise<void>;
|
44
|
+
|
45
|
+
export interface ClusterWorkerProperties {
|
46
|
+
labels: string[];
|
47
|
+
}
|
48
|
+
|
49
|
+
export interface ClusterWorker extends ClusterWorkerProperties {
|
50
|
+
peer: RpcPeer;
|
51
|
+
}
|
52
|
+
|
53
|
+
export function getScryptedClusterMode(): ['server' | 'client', string, number] {
|
54
|
+
const mode = process.env.SCRYPTED_CLUSTER_MODE as 'server' | 'client';
|
55
|
+
if (!mode)
|
56
|
+
return;
|
57
|
+
|
58
|
+
if (!['server', 'client'].includes(mode))
|
59
|
+
throw new Error('SCRYPTED_CLUSTER_MODE must be set to either "server" or "client".');
|
60
|
+
|
61
|
+
const [server, sport] = process.env.SCRYPTED_CLUSTER_SERVER?.split(':') || [];
|
62
|
+
const port = parseInt(sport) || 10556;
|
63
|
+
if (!net.isIP(server)) {
|
64
|
+
if (server)
|
65
|
+
throw new Error('SCRYPTED_CLUSTER_SERVER is set but is not a valid IP address.');
|
66
|
+
if (process.env.SCRYPTED_CLUSTER_SECRET)
|
67
|
+
throw new Error('SCRYPTED_CLUSTER_SECRET is set but SCRYPTED_CLUSTER_SERVER is not set.');
|
68
|
+
return;
|
69
|
+
}
|
70
|
+
if (!process.env.SCRYPTED_CLUSTER_SECRET)
|
71
|
+
throw new Error('SCRYPTED_CLUSTER_SERVER is set but SCRYPTED_CLUSTER_SECRET is not set.');
|
72
|
+
|
73
|
+
const address = process.env.SCRYPTED_CLUSTER_ADDRESS;
|
74
|
+
if (mode === 'server') {
|
75
|
+
if (address && address !== server)
|
76
|
+
throw new Error('SCRYPTED_CLUSTER_ADDRESS does not match server address. This setting should be removed.');
|
77
|
+
process.env.SCRYPTED_CLUSTER_ADDRESS = address || server;
|
78
|
+
}
|
79
|
+
else if (!net.isIP(address)) {
|
80
|
+
throw new Error('SCRYPTED_CLUSTER_ADDRESS is not set to a valid IP address.');
|
81
|
+
}
|
82
|
+
|
83
|
+
return [mode, server, port];
|
84
|
+
}
|
85
|
+
|
86
|
+
function peerLifecycle(serializer: ReturnType<typeof createRpcDuplexSerializer>, peer: RpcPeer, socket: tls.TLSSocket, type: 'server' | 'client') {
|
87
|
+
serializer.setupRpcPeer(peer);
|
88
|
+
|
89
|
+
socket.on('data', data => serializer.onData(data));
|
90
|
+
|
91
|
+
socket.on('error', e => {
|
92
|
+
peer.kill(e.message);
|
93
|
+
});
|
94
|
+
socket.on('close', () => {
|
95
|
+
peer.kill(`cluster ${type} closed`);
|
96
|
+
});
|
97
|
+
peer.killed.then(() => {
|
98
|
+
socket.destroy();
|
99
|
+
});
|
100
|
+
}
|
101
|
+
|
102
|
+
function preparePeer(socket: tls.TLSSocket, type: 'server' | 'client') {
|
103
|
+
const serializer = createRpcDuplexSerializer(socket);
|
104
|
+
const peer = new RpcPeer('cluster-remote', 'cluster-host', (message, reject, serializationContext) => {
|
105
|
+
serializer.sendMessage(message, reject, serializationContext);
|
106
|
+
});
|
107
|
+
|
108
|
+
peerLifecycle(serializer, peer, socket, type);
|
109
|
+
|
110
|
+
return peer;
|
111
|
+
}
|
112
|
+
|
113
|
+
export interface ClusterForkOptions {
|
114
|
+
runtime?: ForkOptions['runtime'];
|
115
|
+
labels?: ForkOptions['labels'];
|
116
|
+
}
|
117
|
+
|
118
|
+
export function matchesClusterLabels(options: ClusterForkOptions, labels: string[]) {
|
119
|
+
let matched = 0;
|
120
|
+
for (const label of options?.labels?.require || []) {
|
121
|
+
if (!labels.includes(label))
|
122
|
+
return 0;
|
123
|
+
}
|
124
|
+
|
125
|
+
// if there is nothing in the any list, consider it matched
|
126
|
+
let foundAny = !options?.labels?.any?.length;
|
127
|
+
for (const label of options.labels?.any || []) {
|
128
|
+
if (!labels.includes(label)) {
|
129
|
+
matched++;
|
130
|
+
foundAny = true;
|
131
|
+
}
|
132
|
+
}
|
133
|
+
if (!foundAny)
|
134
|
+
return 0;
|
135
|
+
|
136
|
+
for (const label of options?.labels?.prefer || []) {
|
137
|
+
if (labels.includes(label))
|
138
|
+
matched++;
|
139
|
+
}
|
140
|
+
// ensure non zero result.
|
141
|
+
matched++;
|
142
|
+
return matched;
|
143
|
+
}
|
144
|
+
|
145
|
+
export function getClusterLabels() {
|
146
|
+
let labels = process.env.SCRYPTED_CLUSTER_LABELS?.split(',') || [];
|
147
|
+
labels.push(process.arch, process.platform, os.hostname());
|
148
|
+
labels = [...new Set(labels)];
|
149
|
+
return labels;
|
150
|
+
}
|
151
|
+
|
152
|
+
type ConnectForkWorker = (auth: ClusterObject, properties: ClusterWorkerProperties) => Promise<{ clusterId: string }>;
|
153
|
+
|
154
|
+
export function startClusterClient(mainFilename: string) {
|
155
|
+
const labels = getClusterLabels();
|
156
|
+
|
157
|
+
const clusterSecret = process.env.SCRYPTED_CLUSTER_SECRET;
|
158
|
+
const clusterMode = getScryptedClusterMode();
|
159
|
+
const [, host, port] = clusterMode;
|
160
|
+
(async () => {
|
161
|
+
while (true) {
|
162
|
+
const backoff = sleep(10000);
|
163
|
+
const socket = tls.connect({
|
164
|
+
host,
|
165
|
+
port,
|
166
|
+
rejectUnauthorized: false,
|
167
|
+
});
|
168
|
+
|
169
|
+
const peer = preparePeer(socket, 'client');
|
170
|
+
|
171
|
+
try {
|
172
|
+
const connectForkWorker: ConnectForkWorker = await peer.getParam('connectForkWorker');
|
173
|
+
const auth: ClusterObject = {
|
174
|
+
address: socket.localAddress,
|
175
|
+
port: socket.localPort,
|
176
|
+
id: undefined,
|
177
|
+
proxyId: undefined,
|
178
|
+
sourceKey: undefined,
|
179
|
+
sha256: undefined,
|
180
|
+
};
|
181
|
+
auth.sha256 = computeClusterObjectHash(auth, clusterSecret);
|
182
|
+
|
183
|
+
const properties: ClusterWorkerProperties = {
|
184
|
+
labels,
|
185
|
+
};
|
186
|
+
|
187
|
+
const { clusterId } = await connectForkWorker(auth, properties);
|
188
|
+
const clusterPeerSetup = prepareClusterPeer(peer);
|
189
|
+
await clusterPeerSetup.initializeCluster({ clusterId, clusterSecret });
|
190
|
+
|
191
|
+
const clusterForkParam: ClusterForkParam = async (
|
192
|
+
peerLiveness: PeerLiveness,
|
193
|
+
runtime: string,
|
194
|
+
packageJson: any,
|
195
|
+
zipHash: string,
|
196
|
+
getZip: () => Promise<Buffer>) => {
|
197
|
+
let runtimeWorker: RuntimeWorker;
|
198
|
+
|
199
|
+
const builtins = getBuiltinRuntimeHosts();
|
200
|
+
const rt = builtins.get(runtime);
|
201
|
+
if (!rt)
|
202
|
+
throw new Error('unknown runtime ' + runtime);
|
203
|
+
|
204
|
+
const pluginId: string = packageJson.name;
|
205
|
+
const { zipFile, unzippedPath } = await prepareZip(getPluginVolume(pluginId), zipHash, getZip);
|
206
|
+
|
207
|
+
const volume = getScryptedVolume();
|
208
|
+
const pluginVolume = getPluginVolume(pluginId);
|
209
|
+
|
210
|
+
runtimeWorker = rt(mainFilename, pluginId, {
|
211
|
+
packageJson,
|
212
|
+
env: {
|
213
|
+
SCRYPTED_VOLUME: volume,
|
214
|
+
SCRYPTED_PLUGIN_VOLUME: pluginVolume,
|
215
|
+
},
|
216
|
+
pluginDebug: undefined,
|
217
|
+
zipFile,
|
218
|
+
unzippedPath,
|
219
|
+
zipHash,
|
220
|
+
}, undefined);
|
221
|
+
|
222
|
+
const threadPeer = new RpcPeer('main', 'thread', (message, reject, serializationContext) => runtimeWorker.send(message, reject, serializationContext));
|
223
|
+
runtimeWorker.setupRpcPeer(threadPeer);
|
224
|
+
runtimeWorker.on('exit', () => {
|
225
|
+
threadPeer.kill('worker exited');
|
226
|
+
});
|
227
|
+
runtimeWorker.on('error', e => {
|
228
|
+
threadPeer.kill('worker error ' + e);
|
229
|
+
});
|
230
|
+
threadPeer.killed.catch(() => { }).finally(() => {
|
231
|
+
runtimeWorker.kill();
|
232
|
+
});
|
233
|
+
peerLiveness.waitKilled().catch(() => { }).finally(() => {
|
234
|
+
threadPeer.kill('peer killed');
|
235
|
+
});
|
236
|
+
let getRemote: any;
|
237
|
+
try {
|
238
|
+
const initializeCluster: InitializeCluster = await threadPeer.getParam('initializeCluster');
|
239
|
+
await initializeCluster({ clusterId, clusterSecret });
|
240
|
+
getRemote = await threadPeer.getParam('getRemote');
|
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
|
+
};
|
264
|
+
};
|
265
|
+
|
266
|
+
const result = new ClusterForkResult(threadPeer, threadPeer.killed, clusterGetRemote);
|
267
|
+
return result;
|
268
|
+
};
|
269
|
+
|
270
|
+
peer.params['fork'] = clusterForkParam;
|
271
|
+
|
272
|
+
await peer.killed;
|
273
|
+
}
|
274
|
+
catch (e) {
|
275
|
+
peer.kill(e.message);
|
276
|
+
socket.destroy();
|
277
|
+
}
|
278
|
+
await backoff;
|
279
|
+
}
|
280
|
+
})();
|
281
|
+
}
|
282
|
+
|
283
|
+
export function createClusterServer(runtime: ScryptedRuntime, certificate: ReturnType<typeof createSelfSignedCertificate>) {
|
284
|
+
const server = tls.createServer({
|
285
|
+
key: certificate.serviceKey,
|
286
|
+
cert: certificate.certificate,
|
287
|
+
}, (socket) => {
|
288
|
+
const peer = preparePeer(socket, 'server');
|
289
|
+
|
290
|
+
const connectForkWorker: ConnectForkWorker = async (auth: ClusterObject, properties: ClusterWorkerProperties) => {
|
291
|
+
try {
|
292
|
+
const sha256 = computeClusterObjectHash(auth, runtime.clusterSecret);
|
293
|
+
if (sha256 !== auth.sha256)
|
294
|
+
throw new Error('cluster object hash mismatch');
|
295
|
+
// the remote address may be ipv6 prefixed so use a fuzzy match.
|
296
|
+
// eg ::ffff:192.168.2.124
|
297
|
+
if (!process.env.SCRYPTED_DISABLE_CLUSTER_SERVER_TRUST) {
|
298
|
+
if (auth.port !== socket.remotePort || !socket.remoteAddress.endsWith(auth.address))
|
299
|
+
throw new Error('cluster object address mismatch');
|
300
|
+
}
|
301
|
+
const worker: ClusterWorker = {
|
302
|
+
...properties,
|
303
|
+
peer,
|
304
|
+
};
|
305
|
+
runtime.clusterWorkers.add(worker);
|
306
|
+
peer.killed.then(() => {
|
307
|
+
runtime.clusterWorkers.delete(worker);
|
308
|
+
});
|
309
|
+
socket.on('close', () => {
|
310
|
+
runtime.clusterWorkers.delete(worker);
|
311
|
+
});
|
312
|
+
}
|
313
|
+
catch (e) {
|
314
|
+
peer.kill(e);
|
315
|
+
socket.destroy();
|
316
|
+
}
|
317
|
+
|
318
|
+
return {
|
319
|
+
clusterId: runtime.clusterId,
|
320
|
+
}
|
321
|
+
}
|
322
|
+
peer.params['connectForkWorker'] = connectForkWorker;
|
323
|
+
});
|
324
|
+
|
325
|
+
return server;
|
326
|
+
}
|
@@ -8,10 +8,10 @@ import v8 from 'v8';
|
|
8
8
|
import vm from 'vm';
|
9
9
|
import { PluginError } from './plugin/plugin-error';
|
10
10
|
import { getScryptedVolume } from './plugin/plugin-volume';
|
11
|
+
import { isNodePluginWorkerProcess } from './plugin/runtime/node-fork-worker';
|
11
12
|
import { RPCResultError, startPeriodicGarbageCollection } from './rpc';
|
12
13
|
import type { Runtime } from './scrypted-server-main';
|
13
|
-
import {
|
14
|
-
|
14
|
+
import { getScryptedClusterMode } from './scrypted-cluster';
|
15
15
|
|
16
16
|
function start(mainFilename: string, options?: {
|
17
17
|
onRuntimeCreated?: (runtime: Runtime) => Promise<void>,
|
@@ -28,8 +28,8 @@ function start(mainFilename: string, options?: {
|
|
28
28
|
globalThis.gc = vm.runInNewContext("gc");
|
29
29
|
}
|
30
30
|
|
31
|
-
if (!semver.gte(process.version, '
|
32
|
-
throw new Error('"node" version out of date. Please update node to
|
31
|
+
if (!semver.gte(process.version, '18.0.0')) {
|
32
|
+
throw new Error('"node" version out of date. Please update node to v18 or higher.')
|
33
33
|
}
|
34
34
|
|
35
35
|
// Node 17 changes the dns resolution order to return the record order.
|
@@ -53,20 +53,26 @@ function start(mainFilename: string, options?: {
|
|
53
53
|
const start = require('./scrypted-plugin-main').default;
|
54
54
|
return start(mainFilename);
|
55
55
|
}
|
56
|
-
else {
|
57
|
-
// unhandled rejections are allowed if they are from a rpc/plugin call.
|
58
|
-
process.on('unhandledRejection', error => {
|
59
|
-
if (error?.constructor !== RPCResultError && error?.constructor !== PluginError) {
|
60
|
-
console.error('fatal error', error);
|
61
|
-
throw error;
|
62
|
-
}
|
63
|
-
console.warn('unhandled rejection of RPC Result', error);
|
64
|
-
});
|
65
56
|
|
66
|
-
|
67
|
-
|
68
|
-
|
57
|
+
// unhandled rejections are allowed if they are from a rpc/plugin call.
|
58
|
+
process.on('unhandledRejection', error => {
|
59
|
+
if (error?.constructor !== RPCResultError && error?.constructor !== PluginError) {
|
60
|
+
console.error('fatal error', error);
|
61
|
+
throw error;
|
62
|
+
}
|
63
|
+
console.warn('unhandled rejection of RPC Result', error);
|
64
|
+
});
|
65
|
+
|
66
|
+
dotenv.config({
|
67
|
+
path: path.join(getScryptedVolume(), '.env'),
|
68
|
+
});
|
69
69
|
|
70
|
+
const clusterMode = getScryptedClusterMode();
|
71
|
+
if (clusterMode?.[0] === 'client') {
|
72
|
+
const start = require('./scrypted-cluster-main').default;
|
73
|
+
return start(mainFilename);
|
74
|
+
}
|
75
|
+
else {
|
70
76
|
const start = require('./scrypted-server-main').default;
|
71
77
|
return start(mainFilename, options);
|
72
78
|
}
|
@@ -11,38 +11,24 @@ import net from 'net';
|
|
11
11
|
import os from 'os';
|
12
12
|
import path from 'path';
|
13
13
|
import process from 'process';
|
14
|
-
import semver from 'semver';
|
15
14
|
import { install as installSourceMapSupport } from 'source-map-support';
|
16
15
|
import { createSelfSignedCertificate, CURRENT_SELF_SIGNED_CERTIFICATE_VERSION } from './cert';
|
17
16
|
import { Plugin, ScryptedUser, Settings } from './db-types';
|
18
17
|
import { getUsableNetworkAddresses } from './ip';
|
19
18
|
import Level from './level';
|
20
|
-
import { PluginError } from './plugin/plugin-error';
|
21
19
|
import { getScryptedVolume } from './plugin/plugin-volume';
|
22
|
-
import { RPCResultError } from './rpc';
|
23
20
|
import { ScryptedRuntime } from './runtime';
|
24
21
|
import { SCRYPTED_DEBUG_PORT, SCRYPTED_INSECURE_PORT, SCRYPTED_SECURE_PORT } from './server-settings';
|
25
22
|
import { getNpmPackageInfo } from './services/plugin';
|
26
23
|
import { setScryptedUserPassword, UsersService } from './services/users';
|
27
24
|
import { sleep } from './sleep';
|
28
25
|
import { ONE_DAY_MILLISECONDS, UserToken } from './usertoken';
|
26
|
+
import { createClusterServer, getScryptedClusterMode } from './scrypted-cluster';
|
29
27
|
|
30
28
|
export type Runtime = ScryptedRuntime;
|
31
29
|
|
32
|
-
|
33
|
-
|
34
|
-
}
|
35
|
-
|
36
|
-
process.on('unhandledRejection', error => {
|
37
|
-
if (error?.constructor !== RPCResultError && error?.constructor !== PluginError) {
|
38
|
-
console.error('pending crash', error);
|
39
|
-
throw error;
|
40
|
-
}
|
41
|
-
console.warn('unhandled rejection of RPC Result', error);
|
42
|
-
});
|
43
|
-
|
44
|
-
async function listenServerPort(env: string, port: number, server: any) {
|
45
|
-
server.listen(port);
|
30
|
+
async function listenServerPort(env: string, port: number, server: http.Server | https.Server | net.Server, hostname?: string) {
|
31
|
+
server.listen(port, hostname);
|
46
32
|
try {
|
47
33
|
await once(server, 'listening');
|
48
34
|
}
|
@@ -474,7 +460,7 @@ async function start(mainFilename: string, options?: {
|
|
474
460
|
debugServer.on('connection', resolve);
|
475
461
|
});
|
476
462
|
|
477
|
-
waitDebug.catch(() => {});
|
463
|
+
waitDebug.catch(() => { });
|
478
464
|
|
479
465
|
workerInspectPort = Math.round(Math.random() * 10000) + 30000;
|
480
466
|
try {
|
@@ -737,6 +723,12 @@ async function start(mainFilename: string, options?: {
|
|
737
723
|
await listenServerPort('SCRYPTED_SECURE_PORT', SCRYPTED_SECURE_PORT, secure);
|
738
724
|
await listenServerPort('SCRYPTED_INSECURE_PORT', SCRYPTED_INSECURE_PORT, insecure);
|
739
725
|
|
726
|
+
const clusterMode = getScryptedClusterMode();
|
727
|
+
if (clusterMode?.[0] === 'server') {
|
728
|
+
const clusterServer = createClusterServer(scrypted, keyPair);
|
729
|
+
await listenServerPort('SCRYPTED_CLUSTER_SERVER', clusterMode[2], clusterServer);
|
730
|
+
}
|
731
|
+
|
740
732
|
console.log('#######################################################');
|
741
733
|
console.log(`Scrypted Volume : ${volumeDir}`);
|
742
734
|
console.log(`Scrypted Server (Local) : https://localhost:${SCRYPTED_SECURE_PORT}/`);
|
@@ -746,13 +738,13 @@ async function start(mainFilename: string, options?: {
|
|
746
738
|
console.log(`Version: : ${await scrypted.info.getVersion()}`);
|
747
739
|
console.log('#######################################################');
|
748
740
|
console.log('Scrypted insecure http service port:', SCRYPTED_INSECURE_PORT);
|
749
|
-
console.log('Ports can be changed with environment variables.')
|
750
|
-
console.log('https: $SCRYPTED_SECURE_PORT')
|
751
|
-
console.log('http : $SCRYPTED_INSECURE_PORT')
|
752
|
-
console.log('Certificate can be modified via tls.createSecureContext options in')
|
741
|
+
console.log('Ports can be changed with environment variables.');
|
742
|
+
console.log('https: $SCRYPTED_SECURE_PORT');
|
743
|
+
console.log('http : $SCRYPTED_INSECURE_PORT');
|
744
|
+
console.log('Certificate can be modified via tls.createSecureContext options in');
|
753
745
|
console.log('JSON file located at SCRYPTED_HTTPS_OPTIONS_FILE environment variable:');
|
754
746
|
console.log('export SCRYPTED_HTTPS_OPTIONS_FILE=/path/to/options.json');
|
755
|
-
console.log('https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions')
|
747
|
+
console.log('https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions');
|
756
748
|
console.log('#######################################################');
|
757
749
|
|
758
750
|
return scrypted;
|
package/src/server-settings.ts
CHANGED
@@ -3,6 +3,7 @@ import { getUsableNetworkAddresses } from './ip';
|
|
3
3
|
export const SCRYPTED_INSECURE_PORT = parseInt(process.env.SCRYPTED_INSECURE_PORT) || 11080;
|
4
4
|
export const SCRYPTED_SECURE_PORT = parseInt(process.env.SCRYPTED_SECURE_PORT) || 10443;
|
5
5
|
export const SCRYPTED_DEBUG_PORT = parseInt(process.env.SCRYPTED_DEBUG_PORT) || 10081;
|
6
|
+
export const SCRYPTED_CLUSTER_WORKERS = parseInt(process.env.SCRYPTED_CLUSTER_WORKERS) || 32;
|
6
7
|
|
7
8
|
export function getIpAddress(): string {
|
8
9
|
return getUsableNetworkAddresses()[0];
|
package/src/services/backup.ts
CHANGED
@@ -5,7 +5,6 @@ import Level from '../level';
|
|
5
5
|
import { getPluginsVolume, getScryptedVolume } from '../plugin/plugin-volume';
|
6
6
|
import { ScryptedRuntime } from '../runtime';
|
7
7
|
import { sleep } from '../sleep';
|
8
|
-
import util from 'util';
|
9
8
|
|
10
9
|
export class Backup {
|
11
10
|
constructor(public runtime: ScryptedRuntime) {}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import type { ScryptedRuntime } from "../runtime";
|
2
|
+
import { ClusterForkOptions, ClusterForkParam, matchesClusterLabels, PeerLiveness } from "../scrypted-cluster";
|
3
|
+
|
4
|
+
export class ClusterFork {
|
5
|
+
constructor(public runtime: ScryptedRuntime) { }
|
6
|
+
|
7
|
+
async fork(peerLiveness: PeerLiveness, options: ClusterForkOptions, packageJson: any, zipHash: string, getZip: () => Promise<Buffer>) {
|
8
|
+
const matchingWorkers = [...this.runtime.clusterWorkers].map(worker => ({
|
9
|
+
worker,
|
10
|
+
matches: matchesClusterLabels(options, worker.labels),
|
11
|
+
}))
|
12
|
+
.filter(({ matches }) => matches);
|
13
|
+
matchingWorkers.sort((a, b) => b.worker.labels.length - a.worker.labels.length);
|
14
|
+
const worker = matchingWorkers[0]?.worker;
|
15
|
+
|
16
|
+
if (!worker)
|
17
|
+
throw new Error(`no worker found for cluster labels ${JSON.stringify(options.labels)}`);
|
18
|
+
|
19
|
+
const fork: ClusterForkParam = await worker.peer.getParam('fork');
|
20
|
+
return fork(peerLiveness, options.runtime, packageJson, zipHash, getZip);
|
21
|
+
}
|
22
|
+
}
|
package/src/services/plugin.ts
CHANGED
@@ -174,34 +174,16 @@ export class PluginComponent {
|
|
174
174
|
consoleServer.clear(pluginDevice.nativeId);
|
175
175
|
}
|
176
176
|
|
177
|
-
async getRemoteServicePort(pluginId: string, name: string, ...args: any[]): Promise<number> {
|
177
|
+
async getRemoteServicePort(pluginId: string, name: string, ...args: any[]): Promise<[number, string]> {
|
178
178
|
if (name === 'console') {
|
179
179
|
const consoleServer = await this.scrypted.plugins[pluginId].consoleServer;
|
180
|
-
return consoleServer.readPort;
|
180
|
+
return [consoleServer.readPort, process.env.SCRYPTED_CLUSTER_ADDRESS];
|
181
181
|
}
|
182
182
|
if (name === 'console-writer') {
|
183
183
|
const consoleServer = await this.scrypted.plugins[pluginId].consoleServer;
|
184
|
-
return consoleServer.writePort;
|
184
|
+
return [consoleServer.writePort, process.env.SCRYPTED_CLUSTER_ADDRESS];
|
185
185
|
}
|
186
186
|
|
187
187
|
return this.scrypted.plugins[pluginId].remote.getServicePort(name, ...args);
|
188
188
|
}
|
189
|
-
|
190
|
-
async setHostParam(pluginId: string, name: string, param?: any) {
|
191
|
-
const host = this.scrypted.plugins[pluginId];
|
192
|
-
if (!host)
|
193
|
-
return;
|
194
|
-
|
195
|
-
const key = `oob-param-${name}`;
|
196
|
-
if (param === undefined)
|
197
|
-
delete host.peer.params[key];
|
198
|
-
else
|
199
|
-
host.peer.params[key] = param;
|
200
|
-
}
|
201
|
-
|
202
|
-
async getHostParam(pluginId: string, name: string) {
|
203
|
-
const host = this.scrypted.plugins[pluginId];
|
204
|
-
const key = `oob-param-${name}`;
|
205
|
-
return host?.peer?.params?.[key];
|
206
|
-
}
|
207
189
|
}
|