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.
@@ -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', { busName, busId, participantId });
281
- return { busName, busId, participantId };
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
- const key = decodeValueFromReader(reader, storage);
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sen-ether-client",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Pure JavaScript SEN client for existing kernels over ether",
5
5
  "senCompatibility": {
6
6
  "kernelProtocolVersion": 9,