lup-system
Version:
NodeJS library to retrieve system information and utilization.
371 lines (370 loc) • 16.4 kB
JavaScript
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);
}
;