sen-ether-client 0.1.1 → 0.1.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/bin/node-sen-probe.js +4 -2
- package/lib/client.js +212 -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,14 @@ 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
|
};
|
|
144
240
|
this.processInfo = createProcessInfo(this.options);
|
|
241
|
+
this.interfaceAddress = resolveInterfaceAddress(this.options.interfaceAddress);
|
|
242
|
+
this.busMulticastRange = normalizeMulticastRange(this.options.busMulticastRange);
|
|
145
243
|
this.socket = undefined;
|
|
146
244
|
this.udpSocket = undefined;
|
|
147
245
|
this.receiveBuffer = Buffer.alloc(0);
|
|
@@ -242,6 +340,14 @@ export class EtherClient extends EventEmitter {
|
|
|
242
340
|
udpSocket.close(resolve);
|
|
243
341
|
}));
|
|
244
342
|
}
|
|
343
|
+
for (const busState of this.buses.values()) {
|
|
344
|
+
if (busState.multicastSocket) {
|
|
345
|
+
closing.push(new Promise(resolve => {
|
|
346
|
+
busState.multicastSocket.close(resolve);
|
|
347
|
+
}));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
this.buses.clear();
|
|
245
351
|
|
|
246
352
|
await Promise.allSettled(closing);
|
|
247
353
|
}
|
|
@@ -252,7 +358,7 @@ export class EtherClient extends EventEmitter {
|
|
|
252
358
|
* @param {string} busName
|
|
253
359
|
* @param {{ participantId?: number }} [options]
|
|
254
360
|
*/
|
|
255
|
-
joinBus(busName, options = {}) {
|
|
361
|
+
async joinBus(busName, options = {}) {
|
|
256
362
|
if (!this.socket) {
|
|
257
363
|
throw new Error('EtherClient is not connected');
|
|
258
364
|
}
|
|
@@ -264,10 +370,21 @@ export class EtherClient extends EventEmitter {
|
|
|
264
370
|
busId,
|
|
265
371
|
participantId,
|
|
266
372
|
readyRemoteParticipants: new Set(),
|
|
267
|
-
interests: new Map()
|
|
373
|
+
interests: new Map(),
|
|
374
|
+
multicastSocket: undefined,
|
|
375
|
+
multicastGroup: undefined
|
|
268
376
|
};
|
|
269
377
|
|
|
270
378
|
this.buses.set(busId, bus);
|
|
379
|
+
try {
|
|
380
|
+
if (this.options.busMulticast !== false) {
|
|
381
|
+
await this.#openBusMulticastSocket(bus);
|
|
382
|
+
}
|
|
383
|
+
} catch (error) {
|
|
384
|
+
this.buses.delete(busId);
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
|
|
271
388
|
this.#sendControlPayload(encodeEtherControlMessage({
|
|
272
389
|
type: 'BusJoined',
|
|
273
390
|
value: {
|
|
@@ -277,8 +394,20 @@ export class EtherClient extends EventEmitter {
|
|
|
277
394
|
}
|
|
278
395
|
}));
|
|
279
396
|
|
|
280
|
-
this.emit('busJoinedLocal', {
|
|
281
|
-
|
|
397
|
+
this.emit('busJoinedLocal', {
|
|
398
|
+
busName,
|
|
399
|
+
busId,
|
|
400
|
+
participantId,
|
|
401
|
+
multicastGroup: bus.multicastGroup,
|
|
402
|
+
multicastPort: this.options.busMulticastPort
|
|
403
|
+
});
|
|
404
|
+
return {
|
|
405
|
+
busName,
|
|
406
|
+
busId,
|
|
407
|
+
participantId,
|
|
408
|
+
multicastGroup: bus.multicastGroup,
|
|
409
|
+
multicastPort: this.options.busMulticastPort
|
|
410
|
+
};
|
|
282
411
|
}
|
|
283
412
|
|
|
284
413
|
/**
|
|
@@ -426,6 +555,10 @@ export class EtherClient extends EventEmitter {
|
|
|
426
555
|
}
|
|
427
556
|
}));
|
|
428
557
|
this.buses.delete(busState.busId);
|
|
558
|
+
if (busState.multicastSocket) {
|
|
559
|
+
busState.multicastSocket.close();
|
|
560
|
+
busState.multicastSocket = undefined;
|
|
561
|
+
}
|
|
429
562
|
this.emit('busLeftLocal', {
|
|
430
563
|
busName: busState.busName,
|
|
431
564
|
busId: busState.busId,
|
|
@@ -573,6 +706,81 @@ export class EtherClient extends EventEmitter {
|
|
|
573
706
|
}
|
|
574
707
|
}
|
|
575
708
|
|
|
709
|
+
#onMulticastBusDatagram(busState, message, remote) {
|
|
710
|
+
try {
|
|
711
|
+
if (message.length < 8) {
|
|
712
|
+
throw new RangeError(`SEN multicast bus datagram too small: ${message.length}`);
|
|
713
|
+
}
|
|
714
|
+
const processId = message.readUInt32LE(0);
|
|
715
|
+
const payloadSize = message.readUInt32LE(4);
|
|
716
|
+
if (processId === this.processInfo.processId) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (payloadSize !== message.length - 8) {
|
|
720
|
+
throw new RangeError(
|
|
721
|
+
`SEN multicast bus payload size mismatch: expected ${payloadSize}, got ${message.length - 8}`
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const frame = {
|
|
726
|
+
to: busState.participantId,
|
|
727
|
+
busId: busState.busId,
|
|
728
|
+
message: message.subarray(8)
|
|
729
|
+
};
|
|
730
|
+
const busMessage = decodeBusMessage(frame.message);
|
|
731
|
+
this.emit('busFrame', { ...frame, busMessage, remote, multicast: true });
|
|
732
|
+
if (busMessage.categoryName === 'controlMessage') {
|
|
733
|
+
this.emit('busControlMessage', { ...frame, control: busMessage.control, remote, multicast: true });
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
this.emit(busMessage.categoryName, { ...frame, ...busMessage, remote, multicast: true });
|
|
737
|
+
} catch (error) {
|
|
738
|
+
this.emit('error', error);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
async #openBusMulticastSocket(busState) {
|
|
743
|
+
const group = computeBusMulticastGroup(
|
|
744
|
+
this.processInfo.sessionId,
|
|
745
|
+
busState.busId,
|
|
746
|
+
this.options.discoveryPort,
|
|
747
|
+
this.busMulticastRange
|
|
748
|
+
);
|
|
749
|
+
const port = this.options.busMulticastPort;
|
|
750
|
+
const bindAddress = process.platform === 'win32' ? undefined : group;
|
|
751
|
+
const socket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
|
|
752
|
+
busState.multicastSocket = socket;
|
|
753
|
+
busState.multicastGroup = group;
|
|
754
|
+
|
|
755
|
+
socket.on('message', (message, remote) => this.#onMulticastBusDatagram(busState, message, remote));
|
|
756
|
+
socket.on('error', error => this.emit('error', error));
|
|
757
|
+
|
|
758
|
+
await new Promise((resolve, reject) => {
|
|
759
|
+
const onError = error => {
|
|
760
|
+
socket.off('listening', onListening);
|
|
761
|
+
reject(error);
|
|
762
|
+
};
|
|
763
|
+
const onListening = () => {
|
|
764
|
+
socket.off('error', onError);
|
|
765
|
+
try {
|
|
766
|
+
socket.addMembership(group, this.interfaceAddress);
|
|
767
|
+
socket.setMulticastLoopback(true);
|
|
768
|
+
if (this.interfaceAddress) {
|
|
769
|
+
socket.setMulticastInterface(this.interfaceAddress);
|
|
770
|
+
}
|
|
771
|
+
} catch (error) {
|
|
772
|
+
reject(error);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
resolve();
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
socket.once('error', onError);
|
|
779
|
+
socket.once('listening', onListening);
|
|
780
|
+
socket.bind(port, bindAddress);
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
|
|
576
784
|
#onRemoteParticipantReady(busState, frame, value) {
|
|
577
785
|
if (value.id !== busState.participantId) {
|
|
578
786
|
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) {
|