@scrypted/server 0.123.31 → 0.123.32
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/plugin/device.d.ts +60 -0
- package/dist/plugin/device.js +249 -0
- package/dist/plugin/device.js.map +1 -0
- package/dist/plugin/endpoint.d.ts +45 -0
- package/dist/plugin/endpoint.js +97 -0
- package/dist/plugin/endpoint.js.map +1 -0
- package/dist/plugin/plugin-remote-worker.js +8 -7
- package/dist/plugin/plugin-remote-worker.js.map +1 -1
- package/dist/plugin/plugin-remote.d.ts +2 -51
- package/dist/plugin/plugin-remote.js +6 -337
- package/dist/plugin/plugin-remote.js.map +1 -1
- package/dist/plugin/runtime/cluster-fork-worker.d.ts +3 -3
- package/dist/plugin/runtime/cluster-fork-worker.js +1 -0
- package/dist/plugin/runtime/cluster-fork-worker.js.map +1 -1
- package/dist/plugin/system.js +0 -1
- package/dist/plugin/system.js.map +1 -1
- package/dist/runtime.d.ts +4 -4
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +1 -1
- package/dist/scrypted-cluster-main.d.ts +12 -2
- package/dist/scrypted-cluster-main.js +30 -4
- package/dist/scrypted-cluster-main.js.map +1 -1
- package/dist/services/cluster-fork.d.ts +2 -2
- package/dist/services/cluster-fork.js +4 -3
- package/dist/services/cluster-fork.js.map +1 -1
- package/package.json +2 -2
- package/python/cluster_labels.py +2 -2
- package/python/cluster_setup.py +1 -1
- package/python/plugin_console.py +8 -0
- package/python/plugin_remote.py +102 -33
- package/python/rpc.py +5 -4
- package/src/plugin/device.ts +261 -0
- package/src/plugin/endpoint.ts +109 -0
- package/src/plugin/plugin-remote-worker.ts +14 -10
- package/src/plugin/plugin-remote.ts +6 -364
- package/src/plugin/runtime/cluster-fork-worker.ts +6 -4
- package/src/plugin/system.ts +0 -1
- package/src/runtime.ts +4 -4
- package/src/scrypted-cluster-main.ts +39 -9
- package/src/services/cluster-fork.ts +5 -5
@@ -8,11 +8,12 @@ import { needsClusterForkWorker } 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 type { DeviceManagerImpl } from './device';
|
11
12
|
import { MediaManagerImpl } from './media';
|
12
13
|
import { PluginAPI, PluginAPIProxy, PluginRemote, PluginRemoteLoadZipOptions, PluginZipAPI } from './plugin-api';
|
13
14
|
import { pipeWorkerConsole, prepareConsoles } from './plugin-console';
|
14
15
|
import { getPluginNodePath, installOptionalDependencies } from './plugin-npm-dependencies';
|
15
|
-
import { attachPluginRemote,
|
16
|
+
import { attachPluginRemote, setupPluginRemote } from './plugin-remote';
|
16
17
|
import { createREPLServer } from './plugin-repl';
|
17
18
|
import { getPluginVolume } from './plugin-volume';
|
18
19
|
import { ChildProcessWorker } from './runtime/child-process-worker';
|
@@ -21,6 +22,9 @@ import { NodeThreadWorker } from './runtime/node-thread-worker';
|
|
21
22
|
import { prepareZip } from './runtime/node-worker-common';
|
22
23
|
import { getBuiltinRuntimeHosts } from './runtime/runtime-host';
|
23
24
|
import { RuntimeWorker } 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';
|
24
28
|
|
25
29
|
const serverVersion = require('../../package.json').version;
|
26
30
|
|
@@ -44,10 +48,9 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
|
|
44
48
|
let deviceManager: DeviceManagerImpl;
|
45
49
|
let api: PluginAPI;
|
46
50
|
|
47
|
-
let pluginsPromise: Promise<
|
51
|
+
let pluginsPromise: Promise<PluginComponent>;
|
48
52
|
function getPlugins() {
|
49
|
-
|
50
|
-
pluginsPromise = api.getComponent('plugins');
|
53
|
+
pluginsPromise ||= api.getComponent('plugins');
|
51
54
|
return pluginsPromise;
|
52
55
|
}
|
53
56
|
|
@@ -113,6 +116,7 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
|
|
113
116
|
await initializeCluster(zipOptions);
|
114
117
|
|
115
118
|
scrypted.connectRPCObject = connectRPCObject;
|
119
|
+
scrypted.clusterManager = new ClusterManagerImpl(api);
|
116
120
|
|
117
121
|
if (worker_threads.isMainThread) {
|
118
122
|
const fsDir = path.join(unzippedPath, 'fs')
|
@@ -270,17 +274,17 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
|
|
270
274
|
}
|
271
275
|
}
|
272
276
|
|
273
|
-
// thread workers inherit main console. pipe anything else.
|
274
|
-
if (!(runtimeWorker instanceof NodeThreadWorker)) {
|
275
|
-
const console = options?.id ? getMixinConsole(options.id, options.nativeId) : undefined;
|
276
|
-
pipeWorkerConsole(nativeWorker, console);
|
277
|
-
}
|
278
|
-
|
279
277
|
const localPeer = new RpcPeer('main', 'thread', (message, reject, serializationContext) => runtimeWorker.send(message, reject, serializationContext));
|
280
278
|
runtimeWorker.setupRpcPeer(localPeer);
|
281
279
|
forkPeer = Promise.resolve(localPeer);
|
282
280
|
}
|
283
281
|
|
282
|
+
// thread workers inherit main console. pipe anything else.
|
283
|
+
if (!(runtimeWorker instanceof NodeThreadWorker)) {
|
284
|
+
const console = options?.id ? getMixinConsole(options.id, options.nativeId) : undefined;
|
285
|
+
pipeWorkerConsole(runtimeWorker, console);
|
286
|
+
}
|
287
|
+
|
284
288
|
const result = (async () => {
|
285
289
|
const threadPeer = await forkPeer;
|
286
290
|
|
@@ -1,372 +1,13 @@
|
|
1
|
-
import {
|
1
|
+
import { EventDetails, MediaManager, ScryptedInterface, ScryptedInterfaceProperty, ScryptedNativeId, ScryptedStatic, SystemDeviceState, SystemManager } from '@scrypted/types';
|
2
2
|
import { RpcPeer, RPCResultError } from '../rpc';
|
3
|
-
import { AccessControls } from './acl';
|
4
3
|
import { BufferSerializer } from '../rpc-buffer-serializer';
|
5
|
-
import {
|
4
|
+
import { AccessControls } from './acl';
|
5
|
+
import { DeviceManagerImpl, StorageImpl } from './device';
|
6
|
+
import { EndpointManagerImpl } from './endpoint';
|
7
|
+
import { PluginAPI, PluginHostInfo, PluginRemote, PluginRemoteLoadZipOptions, PluginZipAPI } from './plugin-api';
|
6
8
|
import { createWebSocketClass, WebSocketConnectCallbacks, WebSocketConnection, WebSocketMethods, WebSocketSerializer } from './plugin-remote-websocket';
|
7
|
-
import { checkProperty } from './plugin-state-check';
|
8
9
|
import { SystemManagerImpl } from './system';
|
9
10
|
|
10
|
-
class DeviceLogger implements Logger {
|
11
|
-
nativeId: ScryptedNativeId;
|
12
|
-
api: PluginAPI;
|
13
|
-
logger: Promise<PluginLogger>;
|
14
|
-
|
15
|
-
constructor(api: PluginAPI, nativeId: ScryptedNativeId, public console: any) {
|
16
|
-
this.api = api;
|
17
|
-
this.nativeId = nativeId;
|
18
|
-
}
|
19
|
-
|
20
|
-
async ensureLogger(): Promise<PluginLogger> {
|
21
|
-
if (!this.logger)
|
22
|
-
this.logger = this.api.getLogger(this.nativeId);
|
23
|
-
return await this.logger;
|
24
|
-
}
|
25
|
-
|
26
|
-
async log(level: string, message: string) {
|
27
|
-
(await this.ensureLogger()).log(level, message);
|
28
|
-
}
|
29
|
-
|
30
|
-
a(msg: string): void {
|
31
|
-
this.log('a', msg);
|
32
|
-
}
|
33
|
-
async clear() {
|
34
|
-
(await this.ensureLogger()).clear();
|
35
|
-
}
|
36
|
-
async clearAlert(msg: string) {
|
37
|
-
(await this.ensureLogger()).clearAlert(msg);
|
38
|
-
}
|
39
|
-
async clearAlerts() {
|
40
|
-
(await this.ensureLogger()).clearAlerts();
|
41
|
-
}
|
42
|
-
d(msg: string): void {
|
43
|
-
this.log('d', msg);
|
44
|
-
}
|
45
|
-
e(msg: string): void {
|
46
|
-
this.log('e', msg);
|
47
|
-
}
|
48
|
-
i(msg: string): void {
|
49
|
-
this.log('i', msg);
|
50
|
-
}
|
51
|
-
v(msg: string): void {
|
52
|
-
this.log('v', msg);
|
53
|
-
}
|
54
|
-
w(msg: string): void {
|
55
|
-
this.log('w', msg);
|
56
|
-
}
|
57
|
-
}
|
58
|
-
|
59
|
-
class EndpointManagerImpl implements EndpointManager {
|
60
|
-
deviceManager: DeviceManagerImpl;
|
61
|
-
api: PluginAPI;
|
62
|
-
pluginId: string;
|
63
|
-
mediaManager: MediaManager;
|
64
|
-
|
65
|
-
getEndpoint(nativeId?: ScryptedNativeId) {
|
66
|
-
if (!nativeId)
|
67
|
-
return this.pluginId;
|
68
|
-
const id = this.deviceManager.nativeIds.get(nativeId)?.id;
|
69
|
-
if (!id)
|
70
|
-
throw new Error('invalid nativeId ' + nativeId);
|
71
|
-
if (!nativeId)
|
72
|
-
return this.pluginId;
|
73
|
-
return id;
|
74
|
-
}
|
75
|
-
|
76
|
-
async getUrlSafeIp() {
|
77
|
-
// ipv6 addresses have colons and need to be bracketed for url safety
|
78
|
-
const ip: string = await this.api.getComponent('SCRYPTED_IP_ADDRESS')
|
79
|
-
return ip?.includes(':') ? `[${ip}]` : ip;
|
80
|
-
}
|
81
|
-
|
82
|
-
/**
|
83
|
-
* @deprecated
|
84
|
-
*/
|
85
|
-
async getAuthenticatedPath(nativeId?: ScryptedNativeId): Promise<string> {
|
86
|
-
return this.getPath(nativeId);
|
87
|
-
}
|
88
|
-
|
89
|
-
/**
|
90
|
-
* @deprecated
|
91
|
-
*/
|
92
|
-
async getInsecurePublicLocalEndpoint(nativeId?: ScryptedNativeId): Promise<string> {
|
93
|
-
return this.getLocalEndpoint(nativeId, {
|
94
|
-
insecure: true,
|
95
|
-
public: true,
|
96
|
-
})
|
97
|
-
}
|
98
|
-
|
99
|
-
/**
|
100
|
-
* @deprecated
|
101
|
-
*/
|
102
|
-
async getPublicCloudEndpoint(nativeId?: ScryptedNativeId): Promise<string> {
|
103
|
-
return this.getCloudEndpoint(nativeId, {
|
104
|
-
public: true,
|
105
|
-
});
|
106
|
-
}
|
107
|
-
|
108
|
-
/**
|
109
|
-
* @deprecated
|
110
|
-
*/
|
111
|
-
async getPublicLocalEndpoint(nativeId?: ScryptedNativeId): Promise<string> {
|
112
|
-
return this.getLocalEndpoint(nativeId, {
|
113
|
-
public: true,
|
114
|
-
})
|
115
|
-
}
|
116
|
-
|
117
|
-
/**
|
118
|
-
* @deprecated
|
119
|
-
*/
|
120
|
-
async getPublicPushEndpoint(nativeId?: ScryptedNativeId): Promise<string> {
|
121
|
-
const mo = await this.mediaManager.createMediaObject(Buffer.from(this.getEndpoint(nativeId)), ScryptedMimeTypes.PushEndpoint);
|
122
|
-
return this.mediaManager.convertMediaObjectToUrl(mo, ScryptedMimeTypes.PushEndpoint);
|
123
|
-
}
|
124
|
-
|
125
|
-
async getPath(nativeId?: string, options?: { public?: boolean; }): Promise<string> {
|
126
|
-
return `/endpoint/${this.getEndpoint(nativeId)}/${options?.public ? 'public/' : ''}`
|
127
|
-
}
|
128
|
-
|
129
|
-
async getLocalEndpoint(nativeId?: string, options?: { public?: boolean; insecure?: boolean; }): Promise<string> {
|
130
|
-
const protocol = options?.insecure ? 'http' : 'https';
|
131
|
-
const port = await this.api.getComponent(options?.insecure ? 'SCRYPTED_INSECURE_PORT' : 'SCRYPTED_SECURE_PORT');
|
132
|
-
const path = await this.getPath(nativeId, options);
|
133
|
-
const url = `${protocol}://${await this.getUrlSafeIp()}:${port}${path}`;
|
134
|
-
return url;
|
135
|
-
}
|
136
|
-
|
137
|
-
async getCloudEndpoint(nativeId?: string, options?: { public?: boolean; }): Promise<string> {
|
138
|
-
const local = await this.getLocalEndpoint(nativeId, options);
|
139
|
-
const mo = await this.mediaManager.createMediaObject(Buffer.from(local), ScryptedMimeTypes.LocalUrl);
|
140
|
-
return this.mediaManager.convertMediaObjectToUrl(mo, ScryptedMimeTypes.LocalUrl);
|
141
|
-
}
|
142
|
-
|
143
|
-
async getCloudPushEndpoint(nativeId?: string): Promise<string> {
|
144
|
-
const mo = await this.mediaManager.createMediaObject(Buffer.from(this.getEndpoint(nativeId)), ScryptedMimeTypes.PushEndpoint);
|
145
|
-
return this.mediaManager.convertMediaObjectToUrl(mo, ScryptedMimeTypes.PushEndpoint);
|
146
|
-
}
|
147
|
-
|
148
|
-
async setLocalAddresses(addresses: string[]): Promise<void> {
|
149
|
-
const addressSettings = await this.api.getComponent('addresses');
|
150
|
-
return addressSettings.setLocalAddresses(addresses);
|
151
|
-
}
|
152
|
-
|
153
|
-
async getLocalAddresses(): Promise<string[]> {
|
154
|
-
const addressSettings = await this.api.getComponent('addresses');
|
155
|
-
return await addressSettings.getLocalAddresses() as string[];
|
156
|
-
}
|
157
|
-
|
158
|
-
async setAccessControlAllowOrigin(options: EndpointAccessControlAllowOrigin): Promise<void> {
|
159
|
-
const self = this;
|
160
|
-
const setAccessControlAllowOrigin = await this.deviceManager.systemManager.getComponent('setAccessControlAllowOrigin') as typeof self.setAccessControlAllowOrigin;
|
161
|
-
return setAccessControlAllowOrigin(options);
|
162
|
-
}
|
163
|
-
}
|
164
|
-
|
165
|
-
class DeviceStateProxyHandler implements ProxyHandler<any> {
|
166
|
-
constructor(public deviceManager: DeviceManagerImpl, public id: string,
|
167
|
-
public setState: (property: string, value: any) => Promise<void>) {
|
168
|
-
}
|
169
|
-
|
170
|
-
get?(target: any, p: PropertyKey, receiver: any) {
|
171
|
-
if (p === 'id')
|
172
|
-
return this.id;
|
173
|
-
if (p === RpcPeer.PROPERTY_PROXY_PROPERTIES)
|
174
|
-
return { id: this.id }
|
175
|
-
if (p === 'setState')
|
176
|
-
return this.setState;
|
177
|
-
return this.deviceManager.systemManager.state[this.id][p as string]?.value;
|
178
|
-
}
|
179
|
-
|
180
|
-
set?(target: any, p: PropertyKey, value: any, receiver: any) {
|
181
|
-
checkProperty(p.toString(), value);
|
182
|
-
this.deviceManager.systemManager.state[this.id][p as string] = {
|
183
|
-
value,
|
184
|
-
};
|
185
|
-
this.setState(p.toString(), value);
|
186
|
-
return true;
|
187
|
-
}
|
188
|
-
}
|
189
|
-
|
190
|
-
interface DeviceManagerDevice {
|
191
|
-
id: string;
|
192
|
-
storage: { [key: string]: any };
|
193
|
-
}
|
194
|
-
|
195
|
-
export class DeviceManagerImpl implements DeviceManager {
|
196
|
-
api: PluginAPI;
|
197
|
-
nativeIds = new Map<string, DeviceManagerDevice>();
|
198
|
-
deviceStorage = new Map<string, StorageImpl>();
|
199
|
-
mixinStorage = new Map<string, Map<string, StorageImpl>>();
|
200
|
-
|
201
|
-
constructor(public systemManager: SystemManagerImpl,
|
202
|
-
public getDeviceConsole: (nativeId?: ScryptedNativeId) => Console,
|
203
|
-
public getMixinConsole: (mixinId: string, nativeId?: ScryptedNativeId) => Console) {
|
204
|
-
}
|
205
|
-
|
206
|
-
async requestRestart() {
|
207
|
-
return this.api.requestRestart();
|
208
|
-
}
|
209
|
-
|
210
|
-
getDeviceLogger(nativeId?: ScryptedNativeId): Logger {
|
211
|
-
return new DeviceLogger(this.api, nativeId, this.getDeviceConsole?.(nativeId) || console);
|
212
|
-
}
|
213
|
-
|
214
|
-
getDeviceState(nativeId?: any): DeviceState {
|
215
|
-
const handler = new DeviceStateProxyHandler(this, this.nativeIds.get(nativeId).id,
|
216
|
-
(property, value) => this.api.setState(nativeId, property, value));
|
217
|
-
return new Proxy(handler, handler);
|
218
|
-
}
|
219
|
-
|
220
|
-
createDeviceState(id: string, setState: (property: string, value: any) => Promise<void>): WritableDeviceState {
|
221
|
-
const handler = new DeviceStateProxyHandler(this, id, setState);
|
222
|
-
return new Proxy(handler, handler);
|
223
|
-
}
|
224
|
-
|
225
|
-
getDeviceStorage(nativeId?: any): StorageImpl {
|
226
|
-
let ret = this.deviceStorage.get(nativeId);
|
227
|
-
if (!ret) {
|
228
|
-
ret = new StorageImpl(this, nativeId);
|
229
|
-
this.deviceStorage.set(nativeId, ret);
|
230
|
-
}
|
231
|
-
return ret;
|
232
|
-
}
|
233
|
-
getMixinStorage(id: string, nativeId?: ScryptedNativeId) {
|
234
|
-
let ms = this.mixinStorage.get(nativeId);
|
235
|
-
if (!ms) {
|
236
|
-
ms = new Map();
|
237
|
-
this.mixinStorage.set(nativeId, ms);
|
238
|
-
}
|
239
|
-
let ret = ms.get(id);
|
240
|
-
if (!ret) {
|
241
|
-
ret = new StorageImpl(this, nativeId, `mixin:${id}:`);
|
242
|
-
ms.set(id, ret);
|
243
|
-
}
|
244
|
-
return ret;
|
245
|
-
}
|
246
|
-
pruneMixinStorage() {
|
247
|
-
for (const nativeId of this.nativeIds.keys()) {
|
248
|
-
const storage = this.nativeIds.get(nativeId).storage;
|
249
|
-
for (const key of Object.keys(storage)) {
|
250
|
-
if (!key.startsWith('mixin:'))
|
251
|
-
continue;
|
252
|
-
const [, id,] = key.split(':');
|
253
|
-
// there's no rush to persist this, it will happen automatically on the plugin
|
254
|
-
// persisting something at some point.
|
255
|
-
// the key itself is unreachable due to the device no longer existing.
|
256
|
-
if (id && !this.systemManager.state[id])
|
257
|
-
delete storage[key];
|
258
|
-
}
|
259
|
-
}
|
260
|
-
}
|
261
|
-
async onMixinEvent(id: string, nativeId: ScryptedNativeId, eventInterface: string, eventData: any) {
|
262
|
-
return this.api.onMixinEvent(id, nativeId, eventInterface, eventData);
|
263
|
-
}
|
264
|
-
getNativeIds(): string[] {
|
265
|
-
return Array.from(this.nativeIds.keys());
|
266
|
-
}
|
267
|
-
async onDeviceDiscovered(device: Device) {
|
268
|
-
return this.api.onDeviceDiscovered(device);
|
269
|
-
}
|
270
|
-
async onDeviceRemoved(nativeId: string) {
|
271
|
-
return this.api.onDeviceRemoved(nativeId);
|
272
|
-
}
|
273
|
-
async onDeviceEvent(nativeId: any, eventInterface: any, eventData?: any) {
|
274
|
-
return this.api.onDeviceEvent(nativeId, eventInterface, eventData);
|
275
|
-
}
|
276
|
-
async onDevicesChanged(devices: DeviceManifest) {
|
277
|
-
return this.api.onDevicesChanged(devices);
|
278
|
-
}
|
279
|
-
}
|
280
|
-
|
281
|
-
function toStorageString(value: any) {
|
282
|
-
if (value === null)
|
283
|
-
return 'null';
|
284
|
-
if (value === undefined)
|
285
|
-
return 'undefined';
|
286
|
-
|
287
|
-
return value.toString();
|
288
|
-
}
|
289
|
-
|
290
|
-
class StorageImpl implements Storage {
|
291
|
-
api: PluginAPI;
|
292
|
-
[name: string]: any;
|
293
|
-
|
294
|
-
private static allowedMethods = [
|
295
|
-
'length',
|
296
|
-
'clear',
|
297
|
-
'getItem',
|
298
|
-
'setItem',
|
299
|
-
'key',
|
300
|
-
'removeItem',
|
301
|
-
];
|
302
|
-
private static indexedHandler: ProxyHandler<StorageImpl> = {
|
303
|
-
get(target, property) {
|
304
|
-
const keyString = property.toString();
|
305
|
-
if (StorageImpl.allowedMethods.includes(keyString)) {
|
306
|
-
const f = target[keyString];
|
307
|
-
if (keyString === 'length')
|
308
|
-
return f;
|
309
|
-
return f.bind(target);
|
310
|
-
}
|
311
|
-
return target.getItem(toStorageString(property));
|
312
|
-
},
|
313
|
-
set(target, property, value): boolean {
|
314
|
-
target.setItem(toStorageString(property), value);
|
315
|
-
return true;
|
316
|
-
}
|
317
|
-
};
|
318
|
-
|
319
|
-
constructor(public deviceManager: DeviceManagerImpl, public nativeId: ScryptedNativeId, public prefix?: string) {
|
320
|
-
this.deviceManager = deviceManager;
|
321
|
-
this.api = deviceManager.api;
|
322
|
-
this.nativeId = nativeId;
|
323
|
-
if (!this.prefix)
|
324
|
-
this.prefix = '';
|
325
|
-
|
326
|
-
return new Proxy(this, StorageImpl.indexedHandler);
|
327
|
-
}
|
328
|
-
|
329
|
-
get storage(): { [key: string]: any } {
|
330
|
-
return this.deviceManager.nativeIds.get(this.nativeId).storage;
|
331
|
-
}
|
332
|
-
|
333
|
-
get length(): number {
|
334
|
-
return Object.keys(this.storage).filter(key => key.startsWith(this.prefix)).length;
|
335
|
-
}
|
336
|
-
|
337
|
-
clear(): void {
|
338
|
-
if (!this.prefix) {
|
339
|
-
this.deviceManager.nativeIds.get(this.nativeId).storage = {};
|
340
|
-
}
|
341
|
-
else {
|
342
|
-
const storage = this.storage;
|
343
|
-
Object.keys(this.storage).filter(key => key.startsWith(this.prefix)).forEach(key => delete storage[key]);
|
344
|
-
}
|
345
|
-
this.api.setStorage(this.nativeId, this.storage);
|
346
|
-
}
|
347
|
-
|
348
|
-
getItem(key: string): string {
|
349
|
-
return this.storage[this.prefix + key];
|
350
|
-
}
|
351
|
-
key(index: number): string {
|
352
|
-
if (!this.prefix) {
|
353
|
-
return Object.keys(this.storage)[index];
|
354
|
-
}
|
355
|
-
return Object.keys(this.storage).filter(key => key.startsWith(this.prefix))[index].substring(this.prefix.length);
|
356
|
-
}
|
357
|
-
removeItem(key: string): void {
|
358
|
-
delete this.storage[this.prefix + key];
|
359
|
-
this.api.setStorage(this.nativeId, this.storage);
|
360
|
-
}
|
361
|
-
setItem(key: string, value: string): void {
|
362
|
-
key = toStorageString(key);
|
363
|
-
value = toStorageString(value);
|
364
|
-
if (this.storage[this.prefix + key] === value)
|
365
|
-
return;
|
366
|
-
this.storage[this.prefix + key] = value;
|
367
|
-
this.api.setStorage(this.nativeId, this.storage);
|
368
|
-
}
|
369
|
-
}
|
370
11
|
|
371
12
|
export async function setupPluginRemote(peer: RpcPeer, api: PluginAPI, pluginId: string, hostInfo: PluginHostInfo, getSystemState: () => { [id: string]: { [property: string]: SystemDeviceState } }): Promise<PluginRemote> {
|
372
13
|
try {
|
@@ -517,6 +158,7 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
|
|
517
158
|
deviceManager,
|
518
159
|
endpointManager,
|
519
160
|
mediaManager,
|
161
|
+
clusterManager: undefined,
|
520
162
|
log,
|
521
163
|
pluginHostAPI: api,
|
522
164
|
pluginRemoteAPI: undefined,
|
@@ -1,16 +1,17 @@
|
|
1
|
+
import type { ClusterFork } from "@scrypted/types";
|
1
2
|
import { EventEmitter, PassThrough } from "stream";
|
2
3
|
import { Deferred } from "../../deferred";
|
3
4
|
import { RpcPeer } from "../../rpc";
|
4
|
-
import {
|
5
|
-
import type {
|
5
|
+
import { PeerLiveness } from "../../scrypted-cluster-main";
|
6
|
+
import type { ClusterForkService } from "../../services/cluster-fork";
|
6
7
|
import { writeWorkerGenerator } from "../plugin-console";
|
7
8
|
import type { RuntimeWorker } from "./runtime-worker";
|
8
9
|
|
9
10
|
export function createClusterForkWorker(
|
10
|
-
forkComponentPromise: Promise<
|
11
|
+
forkComponentPromise: Promise<ClusterForkService>,
|
11
12
|
zipHash: string,
|
12
13
|
getZip: () => Promise<Buffer>,
|
13
|
-
options:
|
14
|
+
options: Partial<ClusterFork>,
|
14
15
|
packageJson: any,
|
15
16
|
connectRPCObject: (o: any) => Promise<any>) {
|
16
17
|
const waitKilled = new Deferred<void>();
|
@@ -39,6 +40,7 @@ export function createClusterForkWorker(
|
|
39
40
|
const peerLiveness = new PeerLiveness(waitKilled.promise);
|
40
41
|
const clusterForkResultPromise = forkComponentPromise.then(forkComponent => forkComponent.fork(peerLiveness, {
|
41
42
|
runtime: options.runtime || 'node',
|
43
|
+
id: options.id,
|
42
44
|
...options,
|
43
45
|
}, packageJson, zipHash, getZip));
|
44
46
|
clusterForkResultPromise.catch(() => {});
|
package/src/plugin/system.ts
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
import { EventListener, EventListenerOptions, EventListenerRegister, Logger, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceDescriptor, ScryptedInterfaceDescriptors, ScryptedInterfaceProperty, ScryptedNativeId, SystemDeviceState, SystemManager } from "@scrypted/types";
|
2
2
|
import { EventRegistry } from "../event-registry";
|
3
3
|
import { PrimitiveProxyHandler, RpcPeer } from '../rpc';
|
4
|
-
// import type { PluginComponent } from "../services/plugin";
|
5
4
|
import { getInterfaceMethods, getInterfaceProperties, getPropertyInterfaces, isValidInterfaceMethod, propertyInterfaces } from "./descriptor";
|
6
5
|
import { PluginAPI } from "./plugin-api";
|
7
6
|
|
package/src/runtime.ts
CHANGED
@@ -37,7 +37,7 @@ import { getIpAddress, SCRYPTED_INSECURE_PORT, SCRYPTED_SECURE_PORT } from './se
|
|
37
37
|
import { AddressSettings } from './services/addresses';
|
38
38
|
import { Alerts } from './services/alerts';
|
39
39
|
import { Backup } from './services/backup';
|
40
|
-
import {
|
40
|
+
import { ClusterForkService } from './services/cluster-fork';
|
41
41
|
import { CORSControl } from './services/cors';
|
42
42
|
import { Info } from './services/info';
|
43
43
|
import { getNpmPackageInfo, PluginComponent } from './services/plugin';
|
@@ -45,7 +45,7 @@ import { ServiceControl } from './services/service-control';
|
|
45
45
|
import { UsersService } from './services/users';
|
46
46
|
import { getState, ScryptedStateManager, setState } from './state';
|
47
47
|
import { isClusterAddress } from './cluster/cluster-setup';
|
48
|
-
import {
|
48
|
+
import { RunningClusterWorker } from './scrypted-cluster-main';
|
49
49
|
|
50
50
|
interface DeviceProxyPair {
|
51
51
|
handler: PluginDeviceProxyHandler;
|
@@ -63,7 +63,7 @@ interface HttpPluginData {
|
|
63
63
|
export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
|
64
64
|
clusterId = crypto.randomBytes(3).toString('hex');
|
65
65
|
clusterSecret = process.env.SCRYPTED_CLUSTER_SECRET || crypto.randomBytes(16).toString('hex');
|
66
|
-
clusterWorkers = new Map<string,
|
66
|
+
clusterWorkers = new Map<string, RunningClusterWorker>();
|
67
67
|
plugins: { [id: string]: PluginHost } = {};
|
68
68
|
pluginDevices: { [id: string]: PluginDevice } = {};
|
69
69
|
devices: { [id: string]: DeviceProxyPair } = {};
|
@@ -89,7 +89,7 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
|
|
89
89
|
corsControl = new CORSControl(this);
|
90
90
|
addressSettings = new AddressSettings(this);
|
91
91
|
usersService = new UsersService(this);
|
92
|
-
clusterFork = new
|
92
|
+
clusterFork = new ClusterForkService(this);
|
93
93
|
info = new Info();
|
94
94
|
backup = new Backup(this);
|
95
95
|
pluginHosts = getBuiltinRuntimeHosts();
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import type { ForkOptions } from '@scrypted/types';
|
1
|
+
import type { ClusterManager, ClusterWorker, ForkOptions } from '@scrypted/types';
|
2
2
|
import crypto from 'crypto';
|
3
3
|
import { once } from 'events';
|
4
4
|
import net from 'net';
|
@@ -11,13 +11,15 @@ import { computeClusterObjectHash } from './cluster/cluster-hash';
|
|
11
11
|
import { getClusterLabels } from './cluster/cluster-labels';
|
12
12
|
import { getScryptedClusterMode, InitializeCluster, setupCluster } from './cluster/cluster-setup';
|
13
13
|
import type { ClusterObject } from './cluster/connect-rpc-object';
|
14
|
+
import type { PluginAPI } from './plugin/plugin-api';
|
14
15
|
import { getPluginVolume, getScryptedVolume } from './plugin/plugin-volume';
|
15
16
|
import { prepareZip } from './plugin/runtime/node-worker-common';
|
16
17
|
import { getBuiltinRuntimeHosts } from './plugin/runtime/runtime-host';
|
17
|
-
import { RuntimeWorker } from './plugin/runtime/runtime-worker';
|
18
|
+
import type { RuntimeWorker } from './plugin/runtime/runtime-worker';
|
18
19
|
import { RpcPeer } from './rpc';
|
19
20
|
import { createRpcDuplexSerializer } from './rpc-serializer';
|
20
21
|
import type { ScryptedRuntime } from './runtime';
|
22
|
+
import type { ClusterForkService } from './services/cluster-fork';
|
21
23
|
import { sleep } from './sleep';
|
22
24
|
|
23
25
|
installSourceMapSupport({
|
@@ -64,13 +66,13 @@ export interface ClusterForkOptions {
|
|
64
66
|
clusterWorkerId?: ForkOptions['clusterWorkerId'];
|
65
67
|
}
|
66
68
|
|
67
|
-
type ConnectForkWorker = (auth: ClusterObject, properties: ClusterWorkerProperties) => Promise<{ clusterId: string }>;
|
69
|
+
type ConnectForkWorker = (auth: ClusterObject, properties: ClusterWorkerProperties) => Promise<{ clusterId: string, clusterWorkerId: string }>;
|
68
70
|
|
69
71
|
export interface ClusterWorkerProperties {
|
70
72
|
labels: string[];
|
71
73
|
}
|
72
74
|
|
73
|
-
export interface
|
75
|
+
export interface RunningClusterWorker extends ClusterWorkerProperties {
|
74
76
|
id: string;
|
75
77
|
peer: RpcPeer;
|
76
78
|
forks: Set<ClusterForkOptions>;
|
@@ -157,12 +159,11 @@ export function startClusterClient(mainFilename: string) {
|
|
157
159
|
});
|
158
160
|
|
159
161
|
try {
|
160
|
-
|
161
162
|
const connectForkWorker: ConnectForkWorker = await peer.getParam('connectForkWorker');
|
162
163
|
const auth: ClusterObject = {
|
163
164
|
address: socket.localAddress,
|
164
165
|
port: socket.localPort,
|
165
|
-
id: process.env.
|
166
|
+
id: process.env.SCRYPTED_CLUSTER_CLIENT_NAME || os.hostname(),
|
166
167
|
proxyId: undefined,
|
167
168
|
sourceKey: undefined,
|
168
169
|
sha256: undefined,
|
@@ -173,7 +174,8 @@ export function startClusterClient(mainFilename: string) {
|
|
173
174
|
labels,
|
174
175
|
};
|
175
176
|
|
176
|
-
const { clusterId } = await connectForkWorker(auth, properties);
|
177
|
+
const { clusterId, clusterWorkerId } = await connectForkWorker(auth, properties);
|
178
|
+
process.env.SCRYPTED_CLUSTER_WORKER_ID = clusterWorkerId;
|
177
179
|
const clusterPeerSetup = setupCluster(peer);
|
178
180
|
await clusterPeerSetup.initializeCluster({ clusterId, clusterSecret });
|
179
181
|
|
@@ -207,6 +209,8 @@ export function startClusterClient(mainFilename: string) {
|
|
207
209
|
unzippedPath,
|
208
210
|
zipHash,
|
209
211
|
}, undefined);
|
212
|
+
runtimeWorker.stdout.on('data', data => console.log(data.toString()));
|
213
|
+
runtimeWorker.stderr.on('data', data => console.error(data.toString()));
|
210
214
|
|
211
215
|
const threadPeer = new RpcPeer('main', 'thread', (message, reject, serializationContext) => runtimeWorker.send(message, reject, serializationContext));
|
212
216
|
runtimeWorker.setupRpcPeer(threadPeer);
|
@@ -288,6 +292,7 @@ export function createClusterServer(runtime: ScryptedRuntime, certificate: Retur
|
|
288
292
|
const peer = preparePeer(socket, 'server');
|
289
293
|
|
290
294
|
const connectForkWorker: ConnectForkWorker = async (auth: ClusterObject, properties: ClusterWorkerProperties) => {
|
295
|
+
const id = crypto.randomUUID();
|
291
296
|
try {
|
292
297
|
const sha256 = computeClusterObjectHash(auth, runtime.clusterSecret);
|
293
298
|
if (sha256 !== auth.sha256)
|
@@ -301,8 +306,7 @@ export function createClusterServer(runtime: ScryptedRuntime, certificate: Retur
|
|
301
306
|
if (auth.port !== socket.remotePort || !socket.remoteAddress.endsWith(auth.address))
|
302
307
|
throw new Error('cluster object address mismatch');
|
303
308
|
}
|
304
|
-
const
|
305
|
-
const worker: ClusterWorker = {
|
309
|
+
const worker: RunningClusterWorker = {
|
306
310
|
...properties,
|
307
311
|
// generate a random uuid.
|
308
312
|
id,
|
@@ -325,6 +329,7 @@ export function createClusterServer(runtime: ScryptedRuntime, certificate: Retur
|
|
325
329
|
|
326
330
|
return {
|
327
331
|
clusterId: runtime.clusterId,
|
332
|
+
clusterWorkerId: id,
|
328
333
|
}
|
329
334
|
}
|
330
335
|
peer.params['connectForkWorker'] = connectForkWorker;
|
@@ -332,3 +337,28 @@ export function createClusterServer(runtime: ScryptedRuntime, certificate: Retur
|
|
332
337
|
|
333
338
|
return server;
|
334
339
|
}
|
340
|
+
|
341
|
+
export class ClusterManagerImpl implements ClusterManager {
|
342
|
+
private clusterServicePromise: Promise<ClusterForkService>;
|
343
|
+
|
344
|
+
constructor(private api: PluginAPI) {
|
345
|
+
}
|
346
|
+
|
347
|
+
getClusterWorkerId(): string {
|
348
|
+
return process.env.SCRYPTED_CLUSTER_WORKER_ID;
|
349
|
+
}
|
350
|
+
|
351
|
+
getClusterMode(): 'server' | 'client' | undefined {
|
352
|
+
return getScryptedClusterMode()[0];
|
353
|
+
}
|
354
|
+
|
355
|
+
async getClusterWorkers(): Promise<Record<string, ClusterWorker>> {
|
356
|
+
const clusterFork = await this.getClusterService();
|
357
|
+
return clusterFork.getClusterWorkers();
|
358
|
+
}
|
359
|
+
|
360
|
+
private getClusterService() {
|
361
|
+
this.clusterServicePromise ||= this.api.getComponent('cluster-fork');
|
362
|
+
return this.clusterServicePromise;
|
363
|
+
}
|
364
|
+
}
|
@@ -1,9 +1,8 @@
|
|
1
|
-
import type { ScryptedRuntime } from "../runtime";
|
2
1
|
import { matchesClusterLabels } from "../cluster/cluster-labels";
|
3
|
-
import {
|
4
|
-
import {
|
2
|
+
import type { ScryptedRuntime } from "../runtime";
|
3
|
+
import type { ClusterForkOptions, ClusterForkParam, PeerLiveness, RunningClusterWorker } from "../scrypted-cluster-main";
|
5
4
|
|
6
|
-
export class
|
5
|
+
export class ClusterForkService {
|
7
6
|
constructor(public runtime: ScryptedRuntime) { }
|
8
7
|
|
9
8
|
async fork(peerLiveness: PeerLiveness, options: ClusterForkOptions, packageJson: any, zipHash: string, getZip: () => Promise<Buffer>) {
|
@@ -18,7 +17,7 @@ export class ClusterFork {
|
|
18
17
|
});
|
19
18
|
matchingWorkers.sort((a, b) => b.worker.labels.length - a.worker.labels.length);
|
20
19
|
|
21
|
-
let worker:
|
20
|
+
let worker: RunningClusterWorker;
|
22
21
|
|
23
22
|
// try to keep fork id affinity to single worker if present. this presents the opportunity for
|
24
23
|
// IPC.
|
@@ -36,6 +35,7 @@ export class ClusterFork {
|
|
36
35
|
|
37
36
|
const fork: ClusterForkParam = await worker.peer.getParam('fork');
|
38
37
|
const forkResult = await fork(peerLiveness, options.runtime, packageJson, zipHash, getZip);
|
38
|
+
options.id ||= this.runtime.findPluginDevice(packageJson.name)?._id;
|
39
39
|
worker.forks.add(options);
|
40
40
|
forkResult.waitKilled().catch(() => { }).finally(() => {
|
41
41
|
worker.forks.delete(options);
|