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.
@@ -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', { busName, busId, participantId });
281
- return { busName, busId, participantId };
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
- 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.3",
4
4
  "description": "Pure JavaScript SEN client for existing kernels over ether",
5
5
  "senCompatibility": {
6
6
  "kernelProtocolVersion": 9,