UNPKG

lup-system

Version:

NodeJS library to retrieve system information and utilization.

371 lines (370 loc) 16.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.NET_COMPUTE_UTILIZATION_INTERVAL = void 0; exports.stopNetworkUtilizationComputation = stopNetworkUtilizationComputation; exports.canConnect = canConnect; exports.getPrimaryIp = getPrimaryIp; exports.isPortListendedOn = isPortListendedOn; exports.isPortInUse = isPortInUse; exports.isPublicIp = isPublicIp; exports.getPrimaryNetworkInterfaces = getPrimaryNetworkInterfaces; exports.getNetworkInterfaces = getNetworkInterfaces; const utils_1 = require("./utils"); const dgram_1 = __importDefault(require("dgram")); const promises_1 = __importDefault(require("fs/promises")); const net_1 = __importDefault(require("net")); const os_1 = __importDefault(require("os")); /** Intervall in milliseconds at which network interface utilization is computed. */ exports.NET_COMPUTE_UTILIZATION_INTERVAL = 1000; let NET_LAST_COMPUTE = 0; const NET_LAST_STATS = {}; const NET_BYTES_PER_SECOND = {}; // bytes/s let NET_COMPUTE_RUNNING = false; let NET_COMPUTE_TIMEOUT = null; async function computeNetworkUtilization() { switch (process.platform) { case 'linux': { await Promise.allSettled([ ...Object.keys(NET_LAST_STATS).map(async (nic) => promises_1.default.readFile('/sys/class/net/' + nic + '/statistics/rx_bytes', 'utf8').then((data) => { const receivedBytes = parseInt(data.trim(), 10) || 0; const prevReceived = NET_LAST_STATS[nic]?.receivedBytes; if (prevReceived !== undefined) { NET_BYTES_PER_SECOND[nic].receive = (receivedBytes - prevReceived) / ((Date.now() - NET_LAST_COMPUTE) / 1000); } NET_LAST_STATS[nic].receivedBytes = receivedBytes; })), ...Object.keys(NET_LAST_STATS).map(async (nic) => promises_1.default.readFile('/sys/class/net/' + nic + '/statistics/tx_bytes', 'utf8').then((data) => { const sentBytes = parseInt(data.trim(), 10) || 0; const prevSent = NET_LAST_STATS[nic]?.transmittedBytes; if (prevSent !== undefined) { NET_BYTES_PER_SECOND[nic].transmit = (sentBytes - prevSent) / ((Date.now() - NET_LAST_COMPUTE) / 1000); } NET_LAST_STATS[nic].transmittedBytes = sentBytes; })), ]); break; } case 'win32': { const output = await (0, utils_1.execCommand)('powershell -Command "Get-NetAdapterStatistics | Select-Object Name, ReceivedBytes, SentBytes | Format-List"').catch(() => ''); const lines = output.split('\n'); const durationSec = (Date.now() - NET_LAST_COMPUTE) / 1000; let currNic = null; // tslint:disable-next-line:prefer-for-of for (let i = 0; i < lines.length; i++) { const [key, value] = lines[i].split(' : ').map((part) => part.trim()); if (key === 'Name') { currNic = value; } if (!currNic) continue; if (!NET_LAST_STATS[currNic]) NET_LAST_STATS[currNic] = { receivedBytes: 0, transmittedBytes: 0 }; if (!NET_BYTES_PER_SECOND[currNic]) NET_BYTES_PER_SECOND[currNic] = { receive: 0, transmit: 0 }; if (key === 'ReceivedBytes') { const receivedBytes = parseInt(value, 10) || 0; const prevReceived = NET_LAST_STATS[currNic]?.receivedBytes; if (prevReceived !== undefined) { NET_BYTES_PER_SECOND[currNic].receive = (receivedBytes - prevReceived) / durationSec; } NET_LAST_STATS[currNic].receivedBytes = receivedBytes; } else if (key === 'SentBytes') { const sentBytes = parseInt(value, 10) || 0; const prevSent = NET_LAST_STATS[currNic]?.transmittedBytes; if (prevSent !== undefined) { NET_BYTES_PER_SECOND[currNic].transmit = (sentBytes - prevSent) / durationSec; } NET_LAST_STATS[currNic].transmittedBytes = sentBytes; } } break; } } NET_LAST_COMPUTE = Date.now(); } async function runNetComputeInterval() { NET_COMPUTE_RUNNING = true; await computeNetworkUtilization(); if (NET_COMPUTE_RUNNING) NET_COMPUTE_TIMEOUT = setTimeout(runNetComputeInterval, Math.max(exports.NET_COMPUTE_UTILIZATION_INTERVAL, 1)); } /** * Stops the computation of network utilization. * As soon as the getNetworkInterfaces function is called again, the computation will be restarted. */ function stopNetworkUtilizationComputation() { if (NET_COMPUTE_TIMEOUT) clearTimeout(NET_COMPUTE_TIMEOUT); NET_COMPUTE_TIMEOUT = null; NET_COMPUTE_RUNNING = false; } /** * Tries to connect to a given server over TCP. * * @param port Port number to connect to. * @param host Hostname or IP address of the server (default '127.0.0.1'). * @returns True if the connection was successful, false otherwise. */ async function canConnect(port, host = '127.0.0.1') { return new Promise((resolve) => { const socket = net_1.default.connect(port, host, () => { socket.end(); resolve(true); }); socket.on('error', () => { resolve(false); }); }); } /** * Returns the primary IP address that is configured and * which is most likely to also be the public IP. * * @returns Primary IP address. */ async function getPrimaryIp() { return new Promise((resolve, reject) => { const socket = dgram_1.default.createSocket('udp4'); // Destination doesn't need to be reachable – just needs routing socket.connect(80, '8.8.8.8', () => { try { const address = socket.address(); socket.close(); if (typeof address === 'string') { reject(new Error('Unexpected address format')); } else { resolve(address.address); // local IP used for routing } } catch (err) { reject(err); } }); }); } /** * Checks if a process is listening on a given port. * * @warning If a Docker proxy is involved multiple processes can bind to the same port. * Use canConnect() as an alternative to check if a port is already being listened on. * * @param port Port number to check. * @param bindAddress Address of the interface to bind to (default '0.0.0.0' which binds to all interfaces). * @returns Promise that resolves to true if the port is in use, false otherwise. */ async function isPortListendedOn(port, bindAddress = '0.0.0.0') { const server = net_1.default.createServer(); return new Promise((resolve) => { server.unref(); server.once('error', (err) => { server.close(); if (err.code === 'EADDRINUSE') { resolve(true); } else { resolve(false); } }); server.listen(port, bindAddress, () => { server.close(); resolve(false); }); }); } /** * Uses isPortListenedOn() and canConnect() to determine if a port is in use. * @param port Port number to check. * @param bindAddress Address of the interface to bind to (default '0.0.0.0'). */ async function isPortInUse(port, bindAddress = '0.0.0.0') { // check first if port can be listened on (way faster if port is really used because just a kernel check needed). const isListenedOn = await isPortListendedOn(port, bindAddress); if (isListenedOn) return true; // as backup check if can connect const canConn = await canConnect(port, bindAddress); if (canConn) return true; return false; } /** * Checks if the given IPv4 or IPv6 address is a public IP address. * @param ip The IP address to check. * @returns True if the IP address is public, false otherwise. */ function isPublicIp(ip) { if (!ip) return false; const ipv4Parts = ip.split('.'); if (ipv4Parts.length === 4) { const [a, b] = ipv4Parts.map(Number); // Check for private IP ranges return !(a === 10 || a === 127 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168)); } const ipv6Parts = ip.split(':'); if (ipv6Parts.length > 2) { const [first, second] = ipv6Parts; // Check for unique IPv6 ranges return !(first === '::1' || // Loopback first === 'fc00' || first === 'fd00' || // Unique Local Addresses (first === 'fe80' && second === '::') // Link-Local ); } return false; } /** * Returns the primary network interfaces on the system. * * @returns List of primary NICInfo objects. */ async function getPrimaryNetworkInterfaces() { const allNics = await getNetworkInterfaces(); return allNics.filter((nic) => nic.primary); } /** * Returns information about the network interfaces on the system. * * @returns List of NICInfo objects. */ async function getNetworkInterfaces() { if (!NET_COMPUTE_RUNNING) { await runNetComputeInterval(); // runs the first computation immediately await computeNetworkUtilization(); // run second computation immediately to get initial values } const primaryIp = await getPrimaryIp(); const nics = {}; for (const [name, addresses] of Object.entries(os_1.default.networkInterfaces())) { const nic = { name, addresses: addresses?.map((addr) => ({ type: addr.family.toLowerCase(), mac: addr.mac, ip: addr.address, netmask: addr.netmask, cidr: addr.cidr, internal: addr.internal, public: isPublicIp(addr.address), primary: addr.address === primaryIp, })) || [], primary: false, status: { operational: 'unknown', admin: true, // Default to true, will be updated based on platform cable: false, // Default to false, will be updated based on platform }, physical: !name.startsWith('v') && !name.startsWith('lo'), // Assume non-loopback and non-virtual interfaces are physical }; nics[name] = nic; } switch (process.platform) { case 'linux': { await Promise.allSettled([ ...Object.keys(nics).map(async (name) => promises_1.default.readFile('/sys/class/net/' + name + '/carrier', 'utf8').then((data) => { const present = data.trim() === '1'; nics[name].status.cable = present; if (present) nics[name].physical = true; // If a cable is present, it is a physical interface })), ...Object.keys(nics).map(async (name) => promises_1.default.readFile('/sys/class/net/' + name + '/operstate', 'utf8').then((data) => { nics[name].status.operational = data.trim().toLowerCase(); })), ...Object.keys(nics).map(async (name) => promises_1.default.readFile('/sys/class/net/' + name + '/proto_down', 'utf8').then((data) => { nics[name].status.admin = data.trim() !== '1'; // If proto_down is 1, the interface is administratively down })), ...Object.keys(nics).map(async (name) => promises_1.default.readFile('/sys/class/net/' + name + '/speed', 'utf8').then((data) => { const speed = parseInt(data.trim(), 10); if (!isNaN(speed) && speed > 0) { const bits = speed * 1000000; // Convert from Mbps to bps (bits/s) const bytes = Math.floor(bits / 8); // Convert from bps to Bps (bytes/s) nics[name].speed = { bits, bytes, // Convert from bps to Bps (bytes/s) }; } })), ]); break; } case 'win32': { const additionallyFound = []; // os.networkInterfaces() does not return all interfaces if they have the same MAC address // fetch status await (0, utils_1.execCommand)('powershell -Command "Get-NetAdapter | Format-List"') .then((output) => { const lines = output.split('\n'); let currNic = null; // tslint:disable-next-line:prefer-for-of for (let i = 0; i < lines.length; i++) { const [key, value] = lines[i].split(' : ').map((part) => part.trim()); if (key === 'Name') { currNic = value; } if (!currNic) continue; if (!nics[currNic]) { // if the NIC is not in the list, add it nics[currNic] = { name: currNic, addresses: [], primary: false, status: { operational: 'unknown', admin: true, // Default to true, will be updated based on platform cable: false, // Default to false, will be updated based on platform }, physical: !currNic.startsWith('v') && !currNic.startsWith('lo'), // Assume non-loopback and non-virtual interfaces are physical }; additionallyFound.push(currNic); } if (key.startsWith('InterfaceOperationalStat')) { nics[currNic].status.operational = value.toLowerCase(); } else if (key.startsWith('Admin')) { nics[currNic].status.admin = value.toLowerCase() === 'up'; } else if (key.startsWith('LinkSpeed')) { const speed = parseFloat(value.replace(/[^0-9.]/g, '')); const bits = Math.floor(speed * 1000000000); // Convert from Gbps to bps (bits/s) const bytes = Math.floor(bits / 8); // Convert from bps to Bps (bytes/s) nics[currNic].speed = { bits, bytes, // Convert from bps to Bps (bytes/s) }; // compute utilization const utilization = NET_BYTES_PER_SECOND[currNic]; if (utilization) { nics[currNic].utilization = { receive: utilization.receive / bytes, transmit: utilization.transmit / bytes, }; } } else if (key.startsWith('ConnectorPresent')) { const present = value.toLowerCase() === 'true'; nics[currNic].status.cable = present; if (present) nics[currNic].physical = true; // If a cable is present, it is a physical interface } } }) .catch(() => ''); break; } } // post process for (const key of Object.keys(nics)) { nics[key].primary = nics[key].status.operational === 'up' && (nics[key].physical || nics[key].addresses.find((addr) => addr.primary)) && nics[key].speed ? true : false; } return Object.values(nics); }