102 lines
2.5 KiB
TypeScript
102 lines
2.5 KiB
TypeScript
import dgram from 'node:dgram';
|
|
import { networkInterfaces } from 'node:os';
|
|
|
|
export const DISCOVERY_PORT = 3378;
|
|
export const DISCOVERY_MULTICAST = '239.255.42.99';
|
|
export const DISCOVERY_PREFIX = 'AGENTHUB|';
|
|
|
|
export function getLanIPv4(): string | undefined {
|
|
for (const iface of Object.values(networkInterfaces())) {
|
|
if (!iface) continue;
|
|
for (const entry of iface) {
|
|
if (entry.family === 'IPv4' && !entry.internal) {
|
|
return entry.address;
|
|
}
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function resolveAdvertiseUrl(host: string, port: number): string {
|
|
const advertiseHost = host === '0.0.0.0' || host === '::' ? getLanIPv4() ?? '127.0.0.1' : host;
|
|
return `http://${advertiseHost}:${port}`;
|
|
}
|
|
|
|
export function startDiscoveryBroadcaster(serverUrl: string, options?: { port?: number; intervalMs?: number }) {
|
|
const port = options?.port ?? DISCOVERY_PORT;
|
|
const intervalMs = options?.intervalMs ?? 2000;
|
|
const socket = dgram.createSocket('udp4');
|
|
const message = Buffer.from(`${DISCOVERY_PREFIX}${serverUrl}`);
|
|
|
|
socket.on('error', () => {
|
|
// Discovery is best-effort; ignore network errors.
|
|
});
|
|
|
|
const send = () => {
|
|
try {
|
|
socket.send(message, 0, message.length, port, DISCOVERY_MULTICAST);
|
|
} catch {
|
|
try {
|
|
socket.setBroadcast(true);
|
|
socket.send(message, 0, message.length, port, '255.255.255.255');
|
|
} catch {
|
|
// Ignore send failures.
|
|
}
|
|
}
|
|
};
|
|
|
|
send();
|
|
const interval = setInterval(send, intervalMs);
|
|
|
|
return {
|
|
socket,
|
|
interval,
|
|
stop: () => {
|
|
clearInterval(interval);
|
|
socket.close();
|
|
},
|
|
};
|
|
}
|
|
|
|
export function discoverServer(timeoutMs = 3000, port = DISCOVERY_PORT): Promise<string | undefined> {
|
|
return new Promise((resolve) => {
|
|
const socket = dgram.createSocket('udp4');
|
|
let timer: NodeJS.Timeout;
|
|
|
|
const cleanup = () => {
|
|
try {
|
|
socket.close();
|
|
} catch {
|
|
// Already closed.
|
|
}
|
|
clearTimeout(timer);
|
|
};
|
|
|
|
socket.on('message', (msg) => {
|
|
const text = msg.toString();
|
|
if (text.startsWith(DISCOVERY_PREFIX)) {
|
|
cleanup();
|
|
resolve(text.slice(DISCOVERY_PREFIX.length));
|
|
}
|
|
});
|
|
|
|
socket.on('error', () => {
|
|
cleanup();
|
|
resolve(undefined);
|
|
});
|
|
|
|
socket.bind(port, () => {
|
|
try {
|
|
socket.addMembership(DISCOVERY_MULTICAST);
|
|
} catch {
|
|
// Multicast may be blocked; still try to receive broadcasts.
|
|
}
|
|
});
|
|
|
|
timer = setTimeout(() => {
|
|
cleanup();
|
|
resolve(undefined);
|
|
}, timeoutMs);
|
|
});
|
|
}
|