547 lines
20 KiB
C
547 lines
20 KiB
C
/*
|
||
* openuf - sysinfo.c
|
||
*
|
||
* Reads system statistics for the inform payload.
|
||
*
|
||
* ── CPU: /proc/stat ──────────────────────────────────────────────────
|
||
*
|
||
* Format: cpu user nice system idle iowait irq softirq steal
|
||
*
|
||
* Usage is calculated with two snapshots taken at different times:
|
||
* active = user + nice + system + irq + softirq + steal
|
||
* total = active + idle + iowait
|
||
* usage % = (Δactive / Δtotal) × 100
|
||
*
|
||
* ── Memory: /proc/meminfo ────────────────────────────────────────────
|
||
*
|
||
* MemTotal, MemFree, Buffers, Cached
|
||
* used = total - free - buffers - cached
|
||
*
|
||
* ── Interfaces: /proc/net/dev + /sys/class/net/<iface>/ ─────────────
|
||
*
|
||
* /proc/net/dev → cumulative rx/tx counters
|
||
* /sys/class/net/speed → negotiated speed (Mbps)
|
||
* /sys/class/net/duplex → "full" / "half"
|
||
* /sys/class/net/operstate → "up" / "down" / "unknown"
|
||
* /sys/class/net/address → MAC
|
||
* ioctl SIOCGIFADDR → IP
|
||
*
|
||
* ── Radio: iw dev <iface> info + survey dump ─────────────────────────
|
||
*
|
||
* info: current channel, TX power
|
||
* survey dump: active/busy/tx/rx time → calculate % utilization
|
||
*/
|
||
|
||
#define _GNU_SOURCE
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <unistd.h>
|
||
#include <stdbool.h>
|
||
#include <time.h>
|
||
#include <net/if.h>
|
||
#include <sys/ioctl.h>
|
||
#include <sys/socket.h>
|
||
#include <arpa/inet.h>
|
||
#include <netinet/in.h>
|
||
|
||
#include "sysinfo.h"
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════
|
||
Memory
|
||
═══════════════════════════════════════════════════════════════════ */
|
||
int sysinfo_mem(mem_stats_t *out)
|
||
{
|
||
memset(out, 0, sizeof(*out));
|
||
FILE *f = fopen("/proc/meminfo", "r");
|
||
if (!f) return -1;
|
||
|
||
char line[128];
|
||
while (fgets(line, sizeof(line), f)) {
|
||
long val = 0;
|
||
if (sscanf(line, "MemTotal: %ld kB", &val) == 1) out->total_kb = val;
|
||
else if (sscanf(line, "MemFree: %ld kB", &val) == 1) out->free_kb = val;
|
||
else if (sscanf(line, "Buffers: %ld kB", &val) == 1) out->buffer_kb = val;
|
||
else if (sscanf(line, "Cached: %ld kB", &val) == 1) out->cached_kb = val;
|
||
}
|
||
fclose(f);
|
||
return (out->total_kb > 0) ? 0 : -1;
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════
|
||
CPU
|
||
═══════════════════════════════════════════════════════════════════ */
|
||
typedef struct {
|
||
unsigned long long user, nice, sys, idle, iowait, irq, softirq, steal;
|
||
} cpu_snap_t;
|
||
|
||
static cpu_snap_t g_prev = {0};
|
||
static int g_valid = 0;
|
||
|
||
static int read_cpu(cpu_snap_t *s)
|
||
{
|
||
FILE *f = fopen("/proc/stat", "r");
|
||
if (!f) return -1;
|
||
int r = fscanf(f, "cpu %llu %llu %llu %llu %llu %llu %llu %llu",
|
||
&s->user, &s->nice, &s->sys, &s->idle,
|
||
&s->iowait, &s->irq, &s->softirq, &s->steal);
|
||
fclose(f);
|
||
return (r >= 4) ? 0 : -1;
|
||
}
|
||
|
||
int sysinfo_cpu_percent(void)
|
||
{
|
||
cpu_snap_t cur;
|
||
if (read_cpu(&cur) != 0) return 0;
|
||
|
||
if (!g_valid) { g_prev = cur; g_valid = 1; return 0; }
|
||
|
||
unsigned long long da = (cur.user - g_prev.user)
|
||
+ (cur.nice - g_prev.nice)
|
||
+ (cur.sys - g_prev.sys)
|
||
+ (cur.irq - g_prev.irq)
|
||
+ (cur.softirq - g_prev.softirq)
|
||
+ (cur.steal - g_prev.steal);
|
||
unsigned long long di = (cur.idle - g_prev.idle)
|
||
+ (cur.iowait - g_prev.iowait);
|
||
unsigned long long dt = da + di;
|
||
g_prev = cur;
|
||
return (dt == 0) ? 0 : (int)((da * 100) / dt);
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════
|
||
Network interface
|
||
═══════════════════════════════════════════════════════════════════ */
|
||
static int read_sysfs_str(const char *iface, const char *file,
|
||
char *out, size_t sz)
|
||
{
|
||
char path[128];
|
||
snprintf(path, sizeof(path), "/sys/class/net/%s/%s", iface, file);
|
||
FILE *f = fopen(path, "r");
|
||
if (!f) return -1;
|
||
char buf[64] = {0};
|
||
fgets(buf, sizeof(buf), f);
|
||
fclose(f);
|
||
buf[strcspn(buf, "\r\n")] = '\0';
|
||
strncpy(out, buf, sz - 1);
|
||
return (strlen(out) > 0) ? 0 : -1;
|
||
}
|
||
|
||
static int read_sysfs_int(const char *iface, const char *file)
|
||
{
|
||
char buf[32] = {0};
|
||
if (read_sysfs_str(iface, file, buf, sizeof(buf)) != 0) return -1;
|
||
int v = -1; sscanf(buf, "%d", &v); return v;
|
||
}
|
||
|
||
static void read_ip_ioctl(const char *iface, char *out, size_t sz)
|
||
{
|
||
int fd = socket(AF_INET, SOCK_DGRAM, 0);
|
||
if (fd < 0) return;
|
||
struct ifreq ifr;
|
||
memset(&ifr, 0, sizeof(ifr));
|
||
strncpy(ifr.ifr_name, iface, IFNAMSIZ - 1);
|
||
if (ioctl(fd, SIOCGIFADDR, &ifr) == 0) {
|
||
struct sockaddr_in *sa = (struct sockaddr_in *)&ifr.ifr_addr;
|
||
strncpy(out, inet_ntoa(sa->sin_addr), sz - 1);
|
||
}
|
||
close(fd);
|
||
}
|
||
|
||
int sysinfo_iface(const char *ifname, iface_stats_t *out)
|
||
{
|
||
memset(out, 0, sizeof(*out));
|
||
strncpy(out->name, ifname, sizeof(out->name) - 1);
|
||
|
||
/* MAC, operstate, speed, duplex */
|
||
read_sysfs_str(ifname, "address", out->mac, sizeof(out->mac));
|
||
char opstate[32] = {0};
|
||
read_sysfs_str(ifname, "operstate", opstate, sizeof(opstate));
|
||
out->up = (strcmp(opstate, "up") == 0 || strcmp(opstate, "unknown") == 0);
|
||
int sp = read_sysfs_int(ifname, "speed");
|
||
out->speed = (sp > 0) ? sp : 1000;
|
||
|
||
char dup[16] = {0};
|
||
read_sysfs_str(ifname, "duplex", dup, sizeof(dup));
|
||
out->full_duplex = (strncmp(dup, "full", 4) == 0);
|
||
|
||
/* IP */
|
||
read_ip_ioctl(ifname, out->ip, sizeof(out->ip));
|
||
|
||
/* Counters of /proc/net/dev */
|
||
FILE *f = fopen("/proc/net/dev", "r");
|
||
if (!f) return 0;
|
||
|
||
char line[512];
|
||
fgets(line, sizeof(line), f); /* skip header lines */
|
||
fgets(line, sizeof(line), f);
|
||
|
||
while (fgets(line, sizeof(line), f)) {
|
||
char *colon = strchr(line, ':');
|
||
if (!colon) continue;
|
||
|
||
/* Extract interface name (may have leading spaces) */
|
||
size_t end = colon - line;
|
||
while (end > 0 && line[end-1] == ' ') end--;
|
||
size_t start = 0;
|
||
while (start < end && line[start] == ' ') start++;
|
||
char name[32] = {0};
|
||
size_t nlen = end - start;
|
||
if (nlen >= sizeof(name)) continue;
|
||
strncpy(name, line + start, nlen);
|
||
|
||
if (strcmp(name, ifname) != 0) continue;
|
||
|
||
long long rb,rp,re,rd,rf,rframe,rcomp,rmulti;
|
||
long long tb,tp,te,td,tf,tcol,tcomp,tcarr;
|
||
sscanf(colon+1,
|
||
"%lld %lld %lld %lld %lld %lld %lld %lld"
|
||
" %lld %lld %lld %lld %lld %lld %lld %lld",
|
||
&rb,&rp,&re,&rd,&rf,&rframe,&rcomp,&rmulti,
|
||
&tb,&tp,&te,&td,&tf,&tcol,&tcomp,&tcarr);
|
||
out->rx_bytes = rb; out->rx_packets = rp;
|
||
out->rx_errors = re; out->rx_dropped = rd;
|
||
out->rx_frame = rframe;
|
||
out->rx_multicast= rmulti;
|
||
out->tx_bytes = tb; out->tx_packets = tp;
|
||
out->tx_errors = te; out->tx_dropped = td;
|
||
break;
|
||
}
|
||
fclose(f);
|
||
return 0;
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════
|
||
WiFi Radio
|
||
═══════════════════════════════════════════════════════════════════
|
||
|
||
Survey counters are cumulative. Keep a small per-interface snapshot so
|
||
each inform reports utilization during the latest interval rather than an
|
||
average since the radio was started.
|
||
*/
|
||
typedef struct {
|
||
char iface[32];
|
||
long long active;
|
||
long long busy;
|
||
long long tx;
|
||
long long rx;
|
||
long long sta_tx_duration;
|
||
long long sta_rx_duration;
|
||
struct timespec duration_time;
|
||
int duration_valid;
|
||
int valid;
|
||
} survey_snapshot_t;
|
||
|
||
#define MAX_SURVEY_SNAPSHOTS 8
|
||
static survey_snapshot_t survey_snapshots[MAX_SURVEY_SNAPSHOTS];
|
||
|
||
static int utilization_percent(long long part, long long total)
|
||
{
|
||
if (part <= 0 || total <= 0) return 0;
|
||
long long value = (part * 100 + total / 2) / total;
|
||
if (value < 0) return 0;
|
||
if (value > 100) return 100;
|
||
return (int)value;
|
||
}
|
||
|
||
static int count_antenna_chains(unsigned int mask)
|
||
{
|
||
int count = 0;
|
||
while (mask) {
|
||
count += mask & 1U;
|
||
mask >>= 1;
|
||
}
|
||
return count;
|
||
}
|
||
|
||
/*
|
||
1. iw dev wlan0 info → channel and power
|
||
Example:
|
||
Interface wlan0
|
||
channel 6 (2437 MHz), width: 20 MHz
|
||
txpower 20.00 dBm
|
||
|
||
2. iw dev wlan0 survey dump → channel utilization
|
||
We look for the block with "[in use]":
|
||
frequency: 2437 MHz [in use]
|
||
channel active time: 12345 ms
|
||
channel busy time: 987 ms
|
||
channel transmit time: 456 ms
|
||
channel receive time: 321 ms
|
||
|
||
We calculate:
|
||
cu_total = busy/active × 100
|
||
cu_self_tx = transmit/active × 100
|
||
cu_self_rx = receive/active × 100
|
||
*/
|
||
int sysinfo_radio(const char *iface, radio_stats_t *out)
|
||
{
|
||
memset(out, 0, sizeof(*out));
|
||
strncpy(out->iface, iface, sizeof(out->iface) - 1);
|
||
out->noise = -95;
|
||
|
||
char cmd[128];
|
||
int current_freq = 0;
|
||
int wiphy_index = -1;
|
||
|
||
/* iw dev <iface> info */
|
||
snprintf(cmd, sizeof(cmd), "iw dev %s info 2>/dev/null", iface);
|
||
FILE *p = popen(cmd, "r");
|
||
if (!p) return -1;
|
||
|
||
char line[256];
|
||
while (fgets(line, sizeof(line), p)) {
|
||
int ch; float mhz;
|
||
if (sscanf(line, " channel %d (%f MHz)", &ch, &mhz) == 2) {
|
||
out->channel = ch;
|
||
current_freq = (int)(mhz + 0.5f);
|
||
}
|
||
int index;
|
||
if (sscanf(line, " wiphy %d", &index) == 1)
|
||
wiphy_index = index;
|
||
float tp;
|
||
if (sscanf(line, " txpower %f dBm", &tp) == 1)
|
||
out->tx_power = (int)tp;
|
||
}
|
||
pclose(p);
|
||
|
||
/* Read the physical radio rather than trusting the emulated model. */
|
||
if (wiphy_index >= 0) {
|
||
snprintf(cmd, sizeof(cmd), "iw phy phy%d info 2>/dev/null", wiphy_index);
|
||
p = popen(cmd, "r");
|
||
if (p) {
|
||
unsigned int available_tx = 0, available_rx = 0;
|
||
unsigned int configured_tx = 0, configured_rx = 0;
|
||
int max_mcs_nss = 0;
|
||
while (fgets(line, sizeof(line), p)) {
|
||
unsigned int tx_mask, rx_mask;
|
||
if (sscanf(line, " Configured Antennas: TX 0x%x RX 0x%x",
|
||
&tx_mask, &rx_mask) == 2) {
|
||
configured_tx = tx_mask;
|
||
configured_rx = rx_mask;
|
||
} else if (sscanf(line, " Available Antennas: TX 0x%x RX 0x%x",
|
||
&tx_mask, &rx_mask) == 2) {
|
||
available_tx = tx_mask;
|
||
available_rx = rx_mask;
|
||
}
|
||
|
||
int streams, top_mcs;
|
||
if (sscanf(line, " %d streams: MCS 0-%d", &streams, &top_mcs) == 2 &&
|
||
streams > max_mcs_nss)
|
||
max_mcs_nss = streams;
|
||
if (sscanf(line,
|
||
" HT TX/RX MCS rate indexes supported: 0-%d",
|
||
&top_mcs) == 1) {
|
||
int ht_nss = top_mcs / 8 + 1;
|
||
if (ht_nss > max_mcs_nss) max_mcs_nss = ht_nss;
|
||
}
|
||
}
|
||
pclose(p);
|
||
|
||
out->tx_antennas = count_antenna_chains(
|
||
configured_tx ? configured_tx : available_tx);
|
||
out->rx_antennas = count_antenna_chains(
|
||
configured_rx ? configured_rx : available_rx);
|
||
if (out->tx_antennas && out->rx_antennas)
|
||
out->nss = out->tx_antennas < out->rx_antennas
|
||
? out->tx_antennas : out->rx_antennas;
|
||
else
|
||
out->nss = max_mcs_nss;
|
||
}
|
||
}
|
||
|
||
/* iw dev <iface> survey dump */
|
||
snprintf(cmd, sizeof(cmd), "iw dev %s survey dump 2>/dev/null", iface);
|
||
p = popen(cmd, "r");
|
||
if (!p) return 0;
|
||
|
||
long long active=0, busy=0, tx_t=0, rx_t=0;
|
||
int selected = 0;
|
||
while (fgets(line, sizeof(line), p)) {
|
||
int survey_freq;
|
||
if (sscanf(line, " frequency: %d MHz", &survey_freq) == 1) {
|
||
/* Some drivers omit the optional [in use] marker, especially on
|
||
* the secondary radio. Match the frequency reported by iw info. */
|
||
selected = strstr(line, "[in use]") != NULL ||
|
||
(current_freq > 0 && survey_freq == current_freq);
|
||
if (selected)
|
||
active=busy=tx_t=rx_t=0;
|
||
continue;
|
||
}
|
||
if (!selected) continue;
|
||
float noise; long long val;
|
||
if (sscanf(line, " noise: %f dBm", &noise) == 1) out->noise = (int)noise;
|
||
if (sscanf(line, " channel active time: %lld ms", &val) == 1) active = val;
|
||
if (sscanf(line, " channel busy time: %lld ms", &val) == 1) busy = val;
|
||
if (sscanf(line, " channel transmit time: %lld ms", &val) == 1) tx_t = val;
|
||
if (sscanf(line, " channel receive time: %lld ms", &val) == 1) rx_t = val;
|
||
}
|
||
pclose(p);
|
||
|
||
long long sample_active = active;
|
||
long long sample_busy = busy;
|
||
long long sample_tx = tx_t;
|
||
long long sample_rx = rx_t;
|
||
|
||
survey_snapshot_t *snapshot = NULL;
|
||
survey_snapshot_t *free_slot = NULL;
|
||
for (int i = 0; i < MAX_SURVEY_SNAPSHOTS; i++) {
|
||
if (survey_snapshots[i].valid &&
|
||
!strcmp(survey_snapshots[i].iface, iface)) {
|
||
snapshot = &survey_snapshots[i];
|
||
break;
|
||
}
|
||
if (!survey_snapshots[i].valid && !free_slot)
|
||
free_slot = &survey_snapshots[i];
|
||
}
|
||
if (!snapshot) snapshot = free_slot;
|
||
|
||
if (snapshot && snapshot->valid && active > snapshot->active &&
|
||
busy >= snapshot->busy && tx_t >= snapshot->tx && rx_t >= snapshot->rx) {
|
||
sample_active = active - snapshot->active;
|
||
sample_busy = busy - snapshot->busy;
|
||
sample_tx = tx_t - snapshot->tx;
|
||
sample_rx = rx_t - snapshot->rx;
|
||
}
|
||
|
||
if (snapshot && active > 0) {
|
||
snprintf(snapshot->iface, sizeof(snapshot->iface), "%s", iface);
|
||
snapshot->active = active;
|
||
snapshot->busy = busy;
|
||
snapshot->tx = tx_t;
|
||
snapshot->rx = rx_t;
|
||
snapshot->valid = 1;
|
||
}
|
||
|
||
if (sample_active > 0) {
|
||
out->cu_total = utilization_percent(sample_busy, sample_active);
|
||
out->cu_self_tx = utilization_percent(sample_tx, sample_active);
|
||
out->cu_self_rx = utilization_percent(sample_rx, sample_active);
|
||
}
|
||
|
||
/* Associated clients and counters exposed by nl80211. */
|
||
snprintf(cmd, sizeof(cmd), "iw dev %s station dump 2>/dev/null", iface);
|
||
p = popen(cmd, "r");
|
||
if (p) {
|
||
while (fgets(line, sizeof(line), p)) {
|
||
long long val;
|
||
if (!strncmp(line, "Station ", 8)) {
|
||
out->num_sta++;
|
||
} else if (sscanf(line, " tx packets: %lld", &val) == 1) {
|
||
out->tx_packets += val;
|
||
} else if (sscanf(line, " tx retries: %lld", &val) == 1) {
|
||
out->tx_retries += val;
|
||
} else if (sscanf(line, " tx failed: %lld", &val) == 1) {
|
||
out->tx_failed += val;
|
||
} else if (sscanf(line, " tx duration: %lld us", &val) == 1) {
|
||
out->tx_duration += val;
|
||
} else if (sscanf(line, " rx duration: %lld us", &val) == 1) {
|
||
out->rx_duration += val;
|
||
}
|
||
}
|
||
pclose(p);
|
||
}
|
||
|
||
/* Some drivers expose no survey busy time on their secondary radio but
|
||
* do expose per-station airtime durations. Use those interval counters as
|
||
* a conservative "This AP" fallback; interference remains zero because
|
||
* station data cannot measure neighboring transmitters. */
|
||
struct timespec now;
|
||
if (snapshot && clock_gettime(CLOCK_MONOTONIC, &now) == 0) {
|
||
long long elapsed_us = 0;
|
||
if (snapshot->duration_valid) {
|
||
elapsed_us = (now.tv_sec - snapshot->duration_time.tv_sec) * 1000000LL +
|
||
(now.tv_nsec - snapshot->duration_time.tv_nsec) / 1000LL;
|
||
}
|
||
|
||
if (snapshot->duration_valid && elapsed_us >= 1000000LL &&
|
||
out->tx_duration >= snapshot->sta_tx_duration &&
|
||
out->rx_duration >= snapshot->sta_rx_duration &&
|
||
out->cu_total == 0) {
|
||
long long tx_delta = out->tx_duration - snapshot->sta_tx_duration;
|
||
long long rx_delta = out->rx_duration - snapshot->sta_rx_duration;
|
||
out->cu_self_tx = utilization_percent(tx_delta, elapsed_us);
|
||
out->cu_self_rx = utilization_percent(rx_delta, elapsed_us);
|
||
out->cu_total = out->cu_self_tx + out->cu_self_rx;
|
||
if (out->cu_total > 100) out->cu_total = 100;
|
||
}
|
||
|
||
/* sysinfo_radio() is called more than once while building an inform.
|
||
* Ignore sub-second calls so they do not replace the interval base. */
|
||
if (!snapshot->duration_valid || elapsed_us >= 1000000LL) {
|
||
if (!snapshot->valid) {
|
||
snprintf(snapshot->iface, sizeof(snapshot->iface), "%s", iface);
|
||
snapshot->valid = 1;
|
||
}
|
||
snapshot->sta_tx_duration = out->tx_duration;
|
||
snapshot->sta_rx_duration = out->rx_duration;
|
||
snapshot->duration_time = now;
|
||
snapshot->duration_valid = 1;
|
||
}
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
static int frequency_to_channel(int frequency)
|
||
{
|
||
if (frequency == 2484) return 14;
|
||
if (frequency >= 2412 && frequency <= 2472)
|
||
return (frequency - 2407) / 5;
|
||
if (frequency >= 5000 && frequency <= 5895)
|
||
return (frequency - 5000) / 5;
|
||
if (frequency >= 5955 && frequency <= 7115)
|
||
return (frequency - 5950) / 5;
|
||
return 0;
|
||
}
|
||
|
||
int sysinfo_wifi_scan_cache(const char *iface, wifi_scan_t *out, int max_out)
|
||
{
|
||
if (!iface || !out || max_out <= 0) return 0;
|
||
|
||
char cmd[128];
|
||
snprintf(cmd, sizeof(cmd), "iw dev %s scan dump 2>/dev/null", iface);
|
||
FILE *p = popen(cmd, "r");
|
||
if (!p) return 0;
|
||
|
||
int count = 0;
|
||
wifi_scan_t *cur = NULL;
|
||
char line[512];
|
||
while (fgets(line, sizeof(line), p)) {
|
||
char bssid[32];
|
||
if (sscanf(line, "BSS %31[^ (]", bssid) == 1) {
|
||
if (count >= max_out) {
|
||
cur = NULL;
|
||
continue;
|
||
}
|
||
cur = &out[count++];
|
||
memset(cur, 0, sizeof(*cur));
|
||
snprintf(cur->bssid, sizeof(cur->bssid), "%s", bssid);
|
||
continue;
|
||
}
|
||
if (!cur) continue;
|
||
|
||
int value;
|
||
float signal;
|
||
char essid[64];
|
||
if (sscanf(line, " freq: %d", &value) == 1) {
|
||
cur->frequency = value;
|
||
cur->channel = frequency_to_channel(value);
|
||
} else if (sscanf(line, " signal: %f dBm", &signal) == 1) {
|
||
cur->signal = (int)signal;
|
||
value = (cur->signal + 100) * 2;
|
||
cur->rssi = value < 0 ? 0 : value > 100 ? 100 : value;
|
||
} else if (sscanf(line, " last seen: %d ms ago", &value) == 1) {
|
||
cur->age = value / 1000;
|
||
} else if (sscanf(line, " SSID: %63[^\n]", essid) == 1) {
|
||
snprintf(cur->essid, sizeof(cur->essid), "%s", essid);
|
||
} else if (strstr(line, "capability:") && strstr(line, "Privacy")) {
|
||
cur->secured = true;
|
||
} else if (strstr(line, "RSN:") || strstr(line, "WPA:")) {
|
||
cur->secured = true;
|
||
}
|
||
}
|
||
pclose(p);
|
||
return count;
|
||
}
|