182 lines
4.8 KiB
TypeScript
182 lines
4.8 KiB
TypeScript
|
|
import * as plugins from './smartnetwork.plugins.js';
|
||
|
|
import * as path from 'node:path';
|
||
|
|
import { fileURLToPath } from 'node:url';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Command map for the rustnetwork IPC binary.
|
||
|
|
* Each key maps to { params, result } defining the typed IPC protocol.
|
||
|
|
*/
|
||
|
|
type TNetworkCommands = {
|
||
|
|
healthPing: {
|
||
|
|
params: Record<string, never>;
|
||
|
|
result: { pong: boolean };
|
||
|
|
};
|
||
|
|
ping: {
|
||
|
|
params: { host: string; count?: number; timeoutMs?: number };
|
||
|
|
result: {
|
||
|
|
alive: boolean;
|
||
|
|
times: (number | null)[];
|
||
|
|
min: number | null;
|
||
|
|
max: number | null;
|
||
|
|
avg: number | null;
|
||
|
|
stddev: number | null;
|
||
|
|
packetLoss: number;
|
||
|
|
};
|
||
|
|
};
|
||
|
|
traceroute: {
|
||
|
|
params: { host: string; maxHops?: number; timeoutMs?: number };
|
||
|
|
result: {
|
||
|
|
hops: Array<{ ttl: number; ip: string; rtt: number | null }>;
|
||
|
|
};
|
||
|
|
};
|
||
|
|
tcpPortCheck: {
|
||
|
|
params: { host: string; port: number; timeoutMs?: number };
|
||
|
|
result: { isOpen: boolean; latencyMs: number | null };
|
||
|
|
};
|
||
|
|
isLocalPortFree: {
|
||
|
|
params: { port: number };
|
||
|
|
result: { free: boolean };
|
||
|
|
};
|
||
|
|
defaultGateway: {
|
||
|
|
params: Record<string, never>;
|
||
|
|
result: {
|
||
|
|
interfaceName: string;
|
||
|
|
addresses: Array<{ family: string; address: string }>;
|
||
|
|
};
|
||
|
|
};
|
||
|
|
};
|
||
|
|
|
||
|
|
function getPlatformSuffix(): string | null {
|
||
|
|
const platform = process.platform;
|
||
|
|
const arch = process.arch;
|
||
|
|
|
||
|
|
const platformMap: Record<string, string> = {
|
||
|
|
linux: 'linux',
|
||
|
|
darwin: 'macos',
|
||
|
|
win32: 'windows',
|
||
|
|
};
|
||
|
|
|
||
|
|
const archMap: Record<string, string> = {
|
||
|
|
x64: 'amd64',
|
||
|
|
arm64: 'arm64',
|
||
|
|
};
|
||
|
|
|
||
|
|
const p = platformMap[platform];
|
||
|
|
const a = archMap[arch];
|
||
|
|
|
||
|
|
if (p && a) {
|
||
|
|
return `${p}_${a}`;
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Singleton bridge to the rustnetwork binary.
|
||
|
|
* Manages the IPC lifecycle for network diagnostics operations.
|
||
|
|
*/
|
||
|
|
export class RustNetworkBridge {
|
||
|
|
private static instance: RustNetworkBridge | null = null;
|
||
|
|
|
||
|
|
public static getInstance(): RustNetworkBridge {
|
||
|
|
if (!RustNetworkBridge.instance) {
|
||
|
|
RustNetworkBridge.instance = new RustNetworkBridge();
|
||
|
|
}
|
||
|
|
return RustNetworkBridge.instance;
|
||
|
|
}
|
||
|
|
|
||
|
|
private bridge: InstanceType<typeof plugins.smartrust.RustBridge<TNetworkCommands>>;
|
||
|
|
|
||
|
|
private constructor() {
|
||
|
|
const packageDir = path.resolve(
|
||
|
|
path.dirname(fileURLToPath(import.meta.url)),
|
||
|
|
'..',
|
||
|
|
);
|
||
|
|
|
||
|
|
const platformSuffix = getPlatformSuffix();
|
||
|
|
const localPaths: string[] = [];
|
||
|
|
|
||
|
|
// Platform-specific cross-compiled binary
|
||
|
|
if (platformSuffix) {
|
||
|
|
localPaths.push(path.join(packageDir, 'dist_rust', `rustnetwork_${platformSuffix}`));
|
||
|
|
}
|
||
|
|
// Native build without suffix
|
||
|
|
localPaths.push(path.join(packageDir, 'dist_rust', 'rustnetwork'));
|
||
|
|
// Local dev paths
|
||
|
|
localPaths.push(path.join(packageDir, 'rust', 'target', 'release', 'rustnetwork'));
|
||
|
|
localPaths.push(path.join(packageDir, 'rust', 'target', 'debug', 'rustnetwork'));
|
||
|
|
|
||
|
|
this.bridge = new plugins.smartrust.RustBridge<TNetworkCommands>({
|
||
|
|
binaryName: 'rustnetwork',
|
||
|
|
cliArgs: ['--management'],
|
||
|
|
requestTimeoutMs: 30_000,
|
||
|
|
readyTimeoutMs: 10_000,
|
||
|
|
localPaths,
|
||
|
|
searchSystemPath: false,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Spawn the Rust binary and wait for it to be ready.
|
||
|
|
*/
|
||
|
|
public async start(): Promise<void> {
|
||
|
|
const ok = await this.bridge.spawn();
|
||
|
|
if (!ok) {
|
||
|
|
throw new Error('Failed to spawn rustnetwork binary');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Kill the Rust binary.
|
||
|
|
*/
|
||
|
|
public async stop(): Promise<void> {
|
||
|
|
this.bridge.kill();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Ensure the bridge is running before sending a command.
|
||
|
|
*/
|
||
|
|
private async ensureRunning(): Promise<void> {
|
||
|
|
// The bridge will throw if not spawned — we just call start() if not yet running
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== Command wrappers =====
|
||
|
|
|
||
|
|
public async ping(
|
||
|
|
host: string,
|
||
|
|
count?: number,
|
||
|
|
timeoutMs?: number,
|
||
|
|
): Promise<TNetworkCommands['ping']['result']> {
|
||
|
|
return this.bridge.sendCommand('ping', { host, count, timeoutMs });
|
||
|
|
}
|
||
|
|
|
||
|
|
public async traceroute(
|
||
|
|
host: string,
|
||
|
|
maxHops?: number,
|
||
|
|
timeoutMs?: number,
|
||
|
|
): Promise<TNetworkCommands['traceroute']['result']> {
|
||
|
|
return this.bridge.sendCommand('traceroute', { host, maxHops, timeoutMs });
|
||
|
|
}
|
||
|
|
|
||
|
|
public async tcpPortCheck(
|
||
|
|
host: string,
|
||
|
|
port: number,
|
||
|
|
timeoutMs?: number,
|
||
|
|
): Promise<TNetworkCommands['tcpPortCheck']['result']> {
|
||
|
|
return this.bridge.sendCommand('tcpPortCheck', { host, port, timeoutMs });
|
||
|
|
}
|
||
|
|
|
||
|
|
public async isLocalPortFree(
|
||
|
|
port: number,
|
||
|
|
): Promise<TNetworkCommands['isLocalPortFree']['result']> {
|
||
|
|
return this.bridge.sendCommand('isLocalPortFree', { port });
|
||
|
|
}
|
||
|
|
|
||
|
|
public async defaultGateway(): Promise<TNetworkCommands['defaultGateway']['result']> {
|
||
|
|
return this.bridge.sendCommand('defaultGateway', {} as Record<string, never>);
|
||
|
|
}
|
||
|
|
|
||
|
|
public async healthPing(): Promise<TNetworkCommands['healthPing']['result']> {
|
||
|
|
return this.bridge.sendCommand('healthPing', {} as Record<string, never>);
|
||
|
|
}
|
||
|
|
}
|