sen-ether-client 0.1.1 → 0.1.3
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/bin/node-sen-probe.js +4 -2
- package/lib/client.js +215 -4
- package/lib/sen.js +9 -4
- package/lib/values.js +1 -3
- package/package.json +1 -1
package/bin/node-sen-probe.js
CHANGED
|
@@ -254,7 +254,9 @@ try {
|
|
|
254
254
|
|
|
255
255
|
const client = new EtherClient({
|
|
256
256
|
sessionName: target.session.name,
|
|
257
|
-
appName: 'sen-ether-probe'
|
|
257
|
+
appName: 'sen-ether-probe',
|
|
258
|
+
interfaceAddress: options.interfaceAddress,
|
|
259
|
+
discoveryPort: options.port
|
|
258
260
|
});
|
|
259
261
|
const remoteBuses = new Set();
|
|
260
262
|
const requestedTypeHashes = new Set();
|
|
@@ -405,7 +407,7 @@ try {
|
|
|
405
407
|
});
|
|
406
408
|
}
|
|
407
409
|
|
|
408
|
-
client.joinBus(bus);
|
|
410
|
+
await client.joinBus(bus);
|
|
409
411
|
await waitForEvent(client, 'busParticipantReady', 5000).catch(error => {
|
|
410
412
|
console.warn(`[warn] ${error.message}; starting interest anyway`);
|
|
411
413
|
});
|
package/lib/client.js
CHANGED
|
@@ -24,6 +24,18 @@ import { crc32 } from './crc32.js';
|
|
|
24
24
|
|
|
25
25
|
const LINUX_OS_KIND = 1;
|
|
26
26
|
const X64_CPU_ARCH = 1;
|
|
27
|
+
const DEFAULT_DISCOVERY_PORT = 60543;
|
|
28
|
+
const DEFAULT_BUS_MULTICAST_PORT = 50985;
|
|
29
|
+
const BUS_HASH_SEED = 15071983;
|
|
30
|
+
const FNV1A_OFFSET_BASIS = 0x811c9dc5;
|
|
31
|
+
const FNV1A_PRIME = 0x01000193;
|
|
32
|
+
const HASH_COMBINE_MAGIC = 0x9e3779b9;
|
|
33
|
+
const DEFAULT_MULTICAST_RANGE = Object.freeze([
|
|
34
|
+
{ min: 224, max: 239 },
|
|
35
|
+
{ min: 0, max: 255 },
|
|
36
|
+
{ min: 0, max: 255 },
|
|
37
|
+
{ min: 0, max: 255 }
|
|
38
|
+
]);
|
|
27
39
|
|
|
28
40
|
function randomUInt32() {
|
|
29
41
|
return randomBytes(4).readUInt32LE(0);
|
|
@@ -57,6 +69,87 @@ function detectCpuArch() {
|
|
|
57
69
|
}
|
|
58
70
|
}
|
|
59
71
|
|
|
72
|
+
function discoveryPortFromEnv() {
|
|
73
|
+
const value = process.env.SEN_ETHER_DISCOVERY_PORT;
|
|
74
|
+
if (!value) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const port = Number(value);
|
|
78
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
79
|
+
throw new Error(`invalid SEN discovery port in environment: ${value}`);
|
|
80
|
+
}
|
|
81
|
+
return port;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolveInterfaceAddress(value) {
|
|
85
|
+
const text = String(value || '').trim();
|
|
86
|
+
if (!text) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(text)) {
|
|
90
|
+
return text;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const interfaces = os.networkInterfaces();
|
|
94
|
+
const candidates = interfaces[text];
|
|
95
|
+
if (!candidates) {
|
|
96
|
+
return text;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const ipv4 = candidates.find(item => (item.family === 'IPv4' || item.family === 4) && !item.internal);
|
|
100
|
+
if (!ipv4) {
|
|
101
|
+
throw new Error(`network interface "${text}" has no non-internal IPv4 address`);
|
|
102
|
+
}
|
|
103
|
+
return ipv4.address;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function normalizeMulticastRange(value) {
|
|
107
|
+
const ranges = Array.isArray(value) && value.length === 4 ? value : DEFAULT_MULTICAST_RANGE;
|
|
108
|
+
return ranges.map((range, index) => {
|
|
109
|
+
const fallback = DEFAULT_MULTICAST_RANGE[index];
|
|
110
|
+
const min = Number(range?.min ?? fallback.min);
|
|
111
|
+
const max = Number(range?.max ?? fallback.max);
|
|
112
|
+
if (!Number.isInteger(min) || !Number.isInteger(max) || min < 0 || max > 255 || min > max) {
|
|
113
|
+
throw new Error(`invalid SEN bus multicast range at byte ${index}`);
|
|
114
|
+
}
|
|
115
|
+
return { min, max };
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function computeByte(range, hashByte) {
|
|
120
|
+
const length = range.max - range.min;
|
|
121
|
+
return length !== 0 ? range.min + (hashByte % length) : range.min;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function hashIntegral(value, byteSize) {
|
|
125
|
+
let hash = FNV1A_OFFSET_BASIS;
|
|
126
|
+
for (let shift = (byteSize - 1) * 8; shift >= 0; shift -= 8) {
|
|
127
|
+
hash ^= (value >>> shift) & 0xff;
|
|
128
|
+
hash = Math.imul(hash, FNV1A_PRIME) >>> 0;
|
|
129
|
+
}
|
|
130
|
+
return hash >>> 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function combineHashed(seed, hashed) {
|
|
134
|
+
return (seed ^ (
|
|
135
|
+
(hashed + HASH_COMBINE_MAGIC + ((seed << 6) >>> 0) + (seed >>> 2)) >>> 0
|
|
136
|
+
)) >>> 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function computeBusMulticastGroup(sessionId, busId, discoveryPort, ranges) {
|
|
140
|
+
let hash = BUS_HASH_SEED;
|
|
141
|
+
hash = combineHashed(hash, hashIntegral(sessionId >>> 0, 4));
|
|
142
|
+
hash = combineHashed(hash, hashIntegral(busId >>> 0, 4));
|
|
143
|
+
hash = combineHashed(hash, hashIntegral(discoveryPort >>> 0, 2));
|
|
144
|
+
const bytes = [
|
|
145
|
+
computeByte(ranges[0], hash & 0xff),
|
|
146
|
+
computeByte(ranges[1], (hash >>> 8) & 0xff),
|
|
147
|
+
computeByte(ranges[2], (hash >>> 16) & 0xff),
|
|
148
|
+
computeByte(ranges[3], (hash >>> 24) & 0xff)
|
|
149
|
+
];
|
|
150
|
+
return bytes.map((byte, index) => Math.min(Math.max(byte, ranges[index].min), ranges[index].max)).join('.');
|
|
151
|
+
}
|
|
152
|
+
|
|
60
153
|
/**
|
|
61
154
|
* Create a ProcessInfo compatible with sen::kernel::getOwnProcessInfo.
|
|
62
155
|
*
|
|
@@ -139,9 +232,17 @@ export class EtherClient extends EventEmitter {
|
|
|
139
232
|
socketKeepAlive: true,
|
|
140
233
|
socketKeepAliveInitialDelayMs: 1000,
|
|
141
234
|
socketIdleTimeoutMs: 0,
|
|
235
|
+
discoveryPort: discoveryPortFromEnv() ?? DEFAULT_DISCOVERY_PORT,
|
|
236
|
+
busMulticastPort: DEFAULT_BUS_MULTICAST_PORT,
|
|
237
|
+
busMulticastRange: DEFAULT_MULTICAST_RANGE,
|
|
142
238
|
...options
|
|
143
239
|
};
|
|
240
|
+
this.options.discoveryPort ??= discoveryPortFromEnv() ?? DEFAULT_DISCOVERY_PORT;
|
|
241
|
+
this.options.busMulticastPort ??= DEFAULT_BUS_MULTICAST_PORT;
|
|
242
|
+
this.options.busMulticastRange ??= DEFAULT_MULTICAST_RANGE;
|
|
144
243
|
this.processInfo = createProcessInfo(this.options);
|
|
244
|
+
this.interfaceAddress = resolveInterfaceAddress(this.options.interfaceAddress);
|
|
245
|
+
this.busMulticastRange = normalizeMulticastRange(this.options.busMulticastRange);
|
|
145
246
|
this.socket = undefined;
|
|
146
247
|
this.udpSocket = undefined;
|
|
147
248
|
this.receiveBuffer = Buffer.alloc(0);
|
|
@@ -242,6 +343,14 @@ export class EtherClient extends EventEmitter {
|
|
|
242
343
|
udpSocket.close(resolve);
|
|
243
344
|
}));
|
|
244
345
|
}
|
|
346
|
+
for (const busState of this.buses.values()) {
|
|
347
|
+
if (busState.multicastSocket) {
|
|
348
|
+
closing.push(new Promise(resolve => {
|
|
349
|
+
busState.multicastSocket.close(resolve);
|
|
350
|
+
}));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
this.buses.clear();
|
|
245
354
|
|
|
246
355
|
await Promise.allSettled(closing);
|
|
247
356
|
}
|
|
@@ -252,7 +361,7 @@ export class EtherClient extends EventEmitter {
|
|
|
252
361
|
* @param {string} busName
|
|
253
362
|
* @param {{ participantId?: number }} [options]
|
|
254
363
|
*/
|
|
255
|
-
joinBus(busName, options = {}) {
|
|
364
|
+
async joinBus(busName, options = {}) {
|
|
256
365
|
if (!this.socket) {
|
|
257
366
|
throw new Error('EtherClient is not connected');
|
|
258
367
|
}
|
|
@@ -264,10 +373,21 @@ export class EtherClient extends EventEmitter {
|
|
|
264
373
|
busId,
|
|
265
374
|
participantId,
|
|
266
375
|
readyRemoteParticipants: new Set(),
|
|
267
|
-
interests: new Map()
|
|
376
|
+
interests: new Map(),
|
|
377
|
+
multicastSocket: undefined,
|
|
378
|
+
multicastGroup: undefined
|
|
268
379
|
};
|
|
269
380
|
|
|
270
381
|
this.buses.set(busId, bus);
|
|
382
|
+
try {
|
|
383
|
+
if (this.options.busMulticast !== false) {
|
|
384
|
+
await this.#openBusMulticastSocket(bus);
|
|
385
|
+
}
|
|
386
|
+
} catch (error) {
|
|
387
|
+
this.buses.delete(busId);
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
|
|
271
391
|
this.#sendControlPayload(encodeEtherControlMessage({
|
|
272
392
|
type: 'BusJoined',
|
|
273
393
|
value: {
|
|
@@ -277,8 +397,20 @@ export class EtherClient extends EventEmitter {
|
|
|
277
397
|
}
|
|
278
398
|
}));
|
|
279
399
|
|
|
280
|
-
this.emit('busJoinedLocal', {
|
|
281
|
-
|
|
400
|
+
this.emit('busJoinedLocal', {
|
|
401
|
+
busName,
|
|
402
|
+
busId,
|
|
403
|
+
participantId,
|
|
404
|
+
multicastGroup: bus.multicastGroup,
|
|
405
|
+
multicastPort: this.options.busMulticastPort
|
|
406
|
+
});
|
|
407
|
+
return {
|
|
408
|
+
busName,
|
|
409
|
+
busId,
|
|
410
|
+
participantId,
|
|
411
|
+
multicastGroup: bus.multicastGroup,
|
|
412
|
+
multicastPort: this.options.busMulticastPort
|
|
413
|
+
};
|
|
282
414
|
}
|
|
283
415
|
|
|
284
416
|
/**
|
|
@@ -426,6 +558,10 @@ export class EtherClient extends EventEmitter {
|
|
|
426
558
|
}
|
|
427
559
|
}));
|
|
428
560
|
this.buses.delete(busState.busId);
|
|
561
|
+
if (busState.multicastSocket) {
|
|
562
|
+
busState.multicastSocket.close();
|
|
563
|
+
busState.multicastSocket = undefined;
|
|
564
|
+
}
|
|
429
565
|
this.emit('busLeftLocal', {
|
|
430
566
|
busName: busState.busName,
|
|
431
567
|
busId: busState.busId,
|
|
@@ -573,6 +709,81 @@ export class EtherClient extends EventEmitter {
|
|
|
573
709
|
}
|
|
574
710
|
}
|
|
575
711
|
|
|
712
|
+
#onMulticastBusDatagram(busState, message, remote) {
|
|
713
|
+
try {
|
|
714
|
+
if (message.length < 8) {
|
|
715
|
+
throw new RangeError(`SEN multicast bus datagram too small: ${message.length}`);
|
|
716
|
+
}
|
|
717
|
+
const processId = message.readUInt32LE(0);
|
|
718
|
+
const payloadSize = message.readUInt32LE(4);
|
|
719
|
+
if (processId === this.processInfo.processId) {
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (payloadSize !== message.length - 8) {
|
|
723
|
+
throw new RangeError(
|
|
724
|
+
`SEN multicast bus payload size mismatch: expected ${payloadSize}, got ${message.length - 8}`
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const frame = {
|
|
729
|
+
to: busState.participantId,
|
|
730
|
+
busId: busState.busId,
|
|
731
|
+
message: message.subarray(8)
|
|
732
|
+
};
|
|
733
|
+
const busMessage = decodeBusMessage(frame.message);
|
|
734
|
+
this.emit('busFrame', { ...frame, busMessage, remote, multicast: true });
|
|
735
|
+
if (busMessage.categoryName === 'controlMessage') {
|
|
736
|
+
this.emit('busControlMessage', { ...frame, control: busMessage.control, remote, multicast: true });
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
this.emit(busMessage.categoryName, { ...frame, ...busMessage, remote, multicast: true });
|
|
740
|
+
} catch (error) {
|
|
741
|
+
this.emit('error', error);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
async #openBusMulticastSocket(busState) {
|
|
746
|
+
const group = computeBusMulticastGroup(
|
|
747
|
+
this.processInfo.sessionId,
|
|
748
|
+
busState.busId,
|
|
749
|
+
this.options.discoveryPort,
|
|
750
|
+
this.busMulticastRange
|
|
751
|
+
);
|
|
752
|
+
const port = this.options.busMulticastPort;
|
|
753
|
+
const bindAddress = process.platform === 'win32' ? undefined : group;
|
|
754
|
+
const socket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
|
|
755
|
+
busState.multicastSocket = socket;
|
|
756
|
+
busState.multicastGroup = group;
|
|
757
|
+
|
|
758
|
+
socket.on('message', (message, remote) => this.#onMulticastBusDatagram(busState, message, remote));
|
|
759
|
+
socket.on('error', error => this.emit('error', error));
|
|
760
|
+
|
|
761
|
+
await new Promise((resolve, reject) => {
|
|
762
|
+
const onError = error => {
|
|
763
|
+
socket.off('listening', onListening);
|
|
764
|
+
reject(error);
|
|
765
|
+
};
|
|
766
|
+
const onListening = () => {
|
|
767
|
+
socket.off('error', onError);
|
|
768
|
+
try {
|
|
769
|
+
socket.addMembership(group, this.interfaceAddress);
|
|
770
|
+
socket.setMulticastLoopback(true);
|
|
771
|
+
if (this.interfaceAddress) {
|
|
772
|
+
socket.setMulticastInterface(this.interfaceAddress);
|
|
773
|
+
}
|
|
774
|
+
} catch (error) {
|
|
775
|
+
reject(error);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
resolve();
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
socket.once('error', onError);
|
|
782
|
+
socket.once('listening', onListening);
|
|
783
|
+
socket.bind(port, bindAddress);
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
|
|
576
787
|
#onRemoteParticipantReady(busState, frame, value) {
|
|
577
788
|
if (value.id !== busState.participantId) {
|
|
578
789
|
return;
|
package/lib/sen.js
CHANGED
|
@@ -402,7 +402,12 @@ export class Sen extends EventEmitter {
|
|
|
402
402
|
appName: config.appName,
|
|
403
403
|
socketKeepAlive: config.socketKeepAlive,
|
|
404
404
|
socketKeepAliveInitialDelayMs: config.socketKeepAliveInitialDelayMs,
|
|
405
|
-
socketIdleTimeoutMs: config.socketIdleTimeoutMs
|
|
405
|
+
socketIdleTimeoutMs: config.socketIdleTimeoutMs,
|
|
406
|
+
interfaceAddress: config.interfaceAddress,
|
|
407
|
+
discoveryPort: config.port,
|
|
408
|
+
busMulticast: config.busMulticast,
|
|
409
|
+
busMulticastPort: config.busMulticastPort,
|
|
410
|
+
busMulticastRange: config.busMulticastRange
|
|
406
411
|
});
|
|
407
412
|
this.client = client;
|
|
408
413
|
this.target = target;
|
|
@@ -457,7 +462,7 @@ export class Sen extends EventEmitter {
|
|
|
457
462
|
|
|
458
463
|
let senBus = this.buses.get(bus);
|
|
459
464
|
if (!senBus) {
|
|
460
|
-
const joined = this.client.joinBus(bus);
|
|
465
|
+
const joined = await this.client.joinBus(bus);
|
|
461
466
|
const participantReadyTimeoutMs = this.#participantReadyTimeout(options, timeout);
|
|
462
467
|
await waitForEvent(this.client, 'busParticipantReady', participantReadyTimeoutMs).catch(error => {
|
|
463
468
|
this.emit('warning', error);
|
|
@@ -511,7 +516,7 @@ export class Sen extends EventEmitter {
|
|
|
511
516
|
|
|
512
517
|
let senBus = this.buses.get(bus);
|
|
513
518
|
if (!senBus) {
|
|
514
|
-
const joined = this.client.joinBus(bus);
|
|
519
|
+
const joined = await this.client.joinBus(bus);
|
|
515
520
|
const participantReadyTimeoutMs = this.#participantReadyTimeout(options, timeout);
|
|
516
521
|
await waitForEvent(this.client, 'busParticipantReady', participantReadyTimeoutMs).catch(error => {
|
|
517
522
|
this.emit('warning', error);
|
|
@@ -1124,7 +1129,7 @@ export class SenBus extends EventEmitter {
|
|
|
1124
1129
|
await this.sen.waitForRemoteBus(this.name, busReadyTimeoutMs).catch(error => {
|
|
1125
1130
|
this.sen.emit('warning', error);
|
|
1126
1131
|
});
|
|
1127
|
-
const joined = this.sen.client.joinBus(this.name);
|
|
1132
|
+
const joined = await this.sen.client.joinBus(this.name);
|
|
1128
1133
|
this.id = joined.busId;
|
|
1129
1134
|
const participantReadyTimeoutMs = Math.min(timeoutMs, this.sen.options.participantReadyTimeoutMs ?? 1000);
|
|
1130
1135
|
await waitForEvent(this.sen.client, 'busParticipantReady', participantReadyTimeoutMs).catch(error => {
|
package/lib/values.js
CHANGED
|
@@ -70,9 +70,7 @@ function collectClassProperties(spec, typeRegistry, seen = new Set()) {
|
|
|
70
70
|
|
|
71
71
|
function decodeEnum(reader, spec) {
|
|
72
72
|
const storage = spec.data.value.storageType;
|
|
73
|
-
|
|
74
|
-
const item = spec.data.value.enums.find(candidate => candidate.key === key);
|
|
75
|
-
return item?.name ?? key;
|
|
73
|
+
return decodeValueFromReader(reader, storage);
|
|
76
74
|
}
|
|
77
75
|
|
|
78
76
|
function decodeStruct(reader, spec, typeRegistry) {
|