@nasriya/atomix
Version:
Composable helper functions for building reliable systems
227 lines (226 loc) • 10.3 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const net_1 = __importDefault(require("net"));
const child_process_1 = require("child_process");
const util_1 = require("util");
const runtime_1 = __importDefault(require("../../runtime/runtime"));
const execAsync = (0, util_1.promisify)(child_process_1.exec);
class NetworkInspector {
#_helpers = {
traceroute: {
parsers: {
parseWindowsTraceroute(lines) {
const hops = [];
const whiteSpace = ' ';
for (const line of lines) {
const parts = line.split(whiteSpace.repeat(4)).map(t => t.trim());
const hopInt = parseInt(parts[0], 10);
if (isNaN(hopInt)) {
continue;
}
const probes = parts.slice(1);
// The last probe contains time and IP separated by 2 spaces
const lastProbe = probes[probes.length - 1];
const lastParts = lastProbe.split(whiteSpace.repeat(2)).map(t => t.trim());
if (lastParts.length < 2)
continue; // invalid format, skip
const ip = lastParts[1];
probes[probes.length - 1] = lastParts[0]; // keep only the time part
// If all probes are '*', skip this hop (timeout)
if (probes.every(p => p === '*'))
continue;
// Parse times, removing any '<' and converting to float
const timesMs = probes.map(p => {
if (p === '*') {
return undefined;
}
; // indicate timeout
return parseFloat(p.replace('<', ''));
}).filter(t => t !== undefined);
hops.push({
hop: hopInt,
ips: [ip], // Windows traceroute usually has one IP per hop
timesMs
});
}
return hops;
},
parseUnixTraceroute(lines) {
const hops = [];
const whiteSpace = ' ';
for (const line of lines) {
const parts = line.split(whiteSpace.repeat(2)).map(t => t.trim());
const hopInt = parseInt(parts[0], 10);
if (isNaN(hopInt)) {
continue;
}
const item = {
hop: hopInt,
ips: [],
timesMs: []
};
const ips = new Set();
const probes = parts.slice(1);
probesLoop: for (const probe of probes) {
if (probe.startsWith('*')) {
continue probesLoop;
}
if (probe.includes('ms')) {
// e.g. "12.345 ms 192.168.1.1"
const [ms, ip] = probe.split('ms').map(t => t.trim());
item.timesMs.push(parseFloat(ms));
if (ip && typeof ip === 'string') {
ips.add(ip);
}
}
else {
// Possibly an IP with no time, e.g. "192.168.1.1"
ips.add(probe);
}
}
item.ips = Array.from(ips);
hops.push(item);
}
return hops;
}
},
exec: async (hostname, platform) => {
let command = '';
if (platform === 'win32') {
command = `tracert -d ${hostname}`;
}
else if (platform === 'linux' || platform === 'darwin') {
command = `traceroute -n ${hostname}`;
}
else {
throw new Error(`Unsupported platform: ${platform}`);
}
const { stdout } = await execAsync(command);
return stdout;
},
prepareLines: (stdout) => {
let lines = stdout.split('\n').map(line => line.trim()).filter(Boolean);
// Remove header lines before hops
// Find first line that starts with a number (hop index)
const firstHopIndex = lines.findIndex(line => /^\d+/.test(line));
if (firstHopIndex > 0) {
lines = lines.slice(firstHopIndex);
}
return lines;
},
parse: (lines, platform) => {
if (platform === 'win32') {
return this.#_helpers.traceroute.parsers.parseWindowsTraceroute(lines);
}
else {
return this.#_helpers.traceroute.parsers.parseUnixTraceroute(lines);
}
},
}
};
/**
* Checks if a TCP port is open on a given host.
*
* This method attempts to establish a TCP connection to the specified host and port.
* If the connection is successful, it resolves to true. If the connection is refused
* or times out, it resolves to false.
*
* @param port - The TCP port number to check on the host. Must be between 0 and 65535.
* @param option - Optional options object containing a hostname and/or timeout value.
* - `hostname`: The hostname of the host to check. Defaults to 'localhost'.
* - `timeout`: The timeout in milliseconds. Defaults to 2000.
*
* @returns {Promise<boolean>} A promise that resolves to true if the port is open, false otherwise.
* @since v1.0.0
*/
async isPortOpen(port, option) {
const hostname = option?.hostname || 'localhost';
const timeout = option?.timeout || 2000;
return new Promise(resolve => {
const socket = net_1.default.createConnection({ port, host: hostname });
let resolved = false;
const resolveOnce = (value) => {
if (!resolved) {
resolved = true;
socket.destroy(); // Ensures socket is fully closed
resolve(value);
}
};
// Bind error listener immediately
socket.once('error', () => resolveOnce(false));
socket.setTimeout(timeout, () => resolveOnce(false));
socket.once('connect', () => resolveOnce(true));
});
}
/**
* Pings a host to check if it is reachable.
*
* This method executes the native `ping` command with the `-c` flag on Unix-like platforms and the `-n` flag on Windows.
* It resolves true if the host is reachable, false otherwise.
*
* @param hostname - The hostname of the host to ping.
*
* @returns {Promise<boolean>} A promise that resolves to true if the host is reachable, false otherwise.
* @throws {Error} If unable to ping the host.
* @since v1.0.0
*/
async pingHost(hostname, timeoutMs = 2000) {
try {
const platform = process.platform;
const countFlag = platform === 'win32' ? '-n' : '-c';
// Timeout flag differs per platform:
// Windows: -w <timeout in ms>
// Unix: -W <timeout in seconds>
const timeoutFlag = platform === 'win32' ? '-w' : '-W';
const timeoutValue = platform === 'win32' ? timeoutMs : Math.ceil(timeoutMs / 1000);
const command = `ping ${countFlag} 1 ${timeoutFlag} ${timeoutValue} ${hostname}`;
try {
await execAsync(command);
return true; // If the command succeeds, host is reachable
}
catch {
return false; // If ping fails or throws, assume host is unreachable
}
}
catch (error) {
if (error instanceof Error) {
error.message = `Unable to ping host ${hostname}: ${error.message}`;
}
throw error;
}
}
/**
* Performs a traceroute to the given hostname.
*
* This method executes the native `traceroute` command on Unix-like platforms and the `tracert` command on Windows.
* It resolves to an array of TracerouteHop objects, each representing a hop in the path from this machine to the
* given hostname. Each hop contains the IP address of the hop, the hostname of the hop if available, and an array
* of times taken to reach the hop in milliseconds.
*
* @param hostname - The hostname to perform a traceroute to.
*
* @returns {Promise<TracerouteHop[]>} A promise that resolves to an array of TracerouteHop objects.
* @throws {Error} If unable to perform the traceroute.
* @since v1.0.0
*/
async traceroute(hostname) {
try {
const os = await runtime_1.default.loadModule('os');
const platform = os.platform();
const stdout = await this.#_helpers.traceroute.exec(hostname, platform);
const lines = this.#_helpers.traceroute.prepareLines(stdout);
return this.#_helpers.traceroute.parse(lines, platform);
}
catch (error) {
if (error instanceof Error) {
error.message = `Unable to traceroute host ${hostname}: ${error.message}`;
}
throw error;
}
}
}
const networkInspector = new NetworkInspector();
exports.default = networkInspector;