Files
zoitechat/plugins/sysinfo/unix/modern-backend.c

1090 lines
21 KiB
C

/*
* Modern Linux SysInfo backend for ZoiteChat.
*
* Uses native kernel/sysfs interfaces and caches static hardware data.
*/
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/statvfs.h>
#include <sys/utsname.h>
#include <unistd.h>
#include <glib.h>
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#if defined(USE_GTK_FRONTEND)
#include <gdk/gdk.h>
#endif
#ifdef HAVE_LIBPCI
#include <pci/pci.h>
#endif
#include "../format.h"
#include "../sysinfo-backend.h"
#define SYSINFO_SEP " \xC2\xB7 "
typedef struct
{
char *os;
char *cpu;
char *gpu;
char *chipset;
char *sound;
GHashTable *pci_names;
} LinuxStaticSnapshot;
static LinuxStaticSnapshot snapshot;
static gsize os_initialized = 0;
static gsize cpu_initialized = 0;
static gsize pci_initialized = 0;
static gsize sound_initialized = 0;
static char *read_trimmed_file (const char *path);
static gboolean read_hex_file (const char *path, guint *value);
static char *join_ptr_array (GPtrArray *items, const char *separator);
static void ptr_array_add_unique (GPtrArray *items, const char *value);
static char *read_os_pretty_name (void);
static char *read_cpu_model (void);
static guint count_physical_cores (void);
static char *build_cpu_string (void);
static char *build_sound_string (void);
static void scan_pci_devices (void);
static void ensure_os_snapshot (void);
static void ensure_cpu_snapshot (void);
static void ensure_pci_snapshot (void);
static void ensure_sound_snapshot (void);
static char *format_usage (guint64 total, guint64 available);
static char *read_driver_name (const char *interface_path);
static char *
read_trimmed_file (const char *path)
{
char *contents = NULL;
gsize length = 0;
if (!g_file_get_contents (path, &contents, &length, NULL))
{
return NULL;
}
g_strstrip (contents);
if (contents[0] == '\0')
{
g_free (contents);
return NULL;
}
return contents;
}
static gboolean
read_hex_file (const char *path, guint *value)
{
char *contents;
char *end = NULL;
guint64 parsed;
contents = read_trimmed_file (path);
if (!contents)
{
return FALSE;
}
errno = 0;
parsed = g_ascii_strtoull (contents, &end, 16);
if (errno != 0 || end == contents)
{
g_free (contents);
return FALSE;
}
*value = (guint) parsed;
g_free (contents);
return TRUE;
}
static char *
join_ptr_array (GPtrArray *items, const char *separator)
{
GString *joined;
guint i;
if (!items || items->len == 0)
{
return NULL;
}
joined = g_string_new (NULL);
for (i = 0; i < items->len; i++)
{
const char *item = g_ptr_array_index (items, i);
if (i != 0)
{
g_string_append (joined, separator);
}
g_string_append (joined, item);
}
return g_string_free (joined, FALSE);
}
static void
ptr_array_add_unique (GPtrArray *items, const char *value)
{
guint i;
if (!value || value[0] == '\0')
{
return;
}
for (i = 0; i < items->len; i++)
{
const char *existing = g_ptr_array_index (items, i);
if (g_strcmp0 (existing, value) == 0)
{
return;
}
}
g_ptr_array_add (items, g_strdup (value));
}
static char *
unquote_os_release_value (const char *value)
{
char *copy;
gsize len;
if (!value)
{
return NULL;
}
copy = g_strdup (value);
g_strstrip (copy);
len = strlen (copy);
if (len >= 2 &&
((copy[0] == '"' && copy[len - 1] == '"') ||
(copy[0] == '\'' && copy[len - 1] == '\'')))
{
copy[len - 1] = '\0';
memmove (copy, copy + 1, len - 1);
}
return copy;
}
static char *
read_pretty_name_from_os_release (const char *path)
{
char *contents = NULL;
char **lines;
char *result = NULL;
guint i;
if (!g_file_get_contents (path, &contents, NULL, NULL))
{
return NULL;
}
lines = g_strsplit (contents, "\n", -1);
for (i = 0; lines[i] != NULL; i++)
{
if (g_str_has_prefix (lines[i], "PRETTY_NAME="))
{
result = unquote_os_release_value (lines[i] + strlen ("PRETTY_NAME="));
break;
}
}
g_strfreev (lines);
g_free (contents);
return result;
}
static char *
read_os_pretty_name (void)
{
char *pretty = NULL;
/* Flatpak exposes the host os-release here. Prefer it over the runtime. */
if (g_file_test ("/run/host/etc/os-release", G_FILE_TEST_IS_REGULAR))
{
pretty = read_pretty_name_from_os_release ("/run/host/etc/os-release");
}
if (!pretty)
{
pretty = g_get_os_info (G_OS_INFO_KEY_PRETTY_NAME);
}
if (!pretty)
{
pretty = read_pretty_name_from_os_release ("/etc/os-release");
}
return pretty ? pretty : g_strdup ("Linux");
}
static char *
read_cpu_model (void)
{
FILE *fp;
char line[1024];
static const char *keys[] = {
"model name",
"Hardware",
"Processor",
"cpu model",
"Model Name",
NULL
};
fp = fopen ("/proc/cpuinfo", "r");
if (fp)
{
while (fgets (line, sizeof (line), fp) != NULL)
{
char *colon = strchr (line, ':');
guint i;
if (!colon)
{
continue;
}
*colon = '\0';
g_strstrip (line);
for (i = 0; keys[i] != NULL; i++)
{
if (g_ascii_strcasecmp (line, keys[i]) == 0)
{
char *value = g_strdup (colon + 1);
char *endptr = NULL;
g_strstrip (value);
/*
* On x86 /proc/cpuinfo starts with `processor : 0`.
* `Processor` is also a useful model key on some ARM systems,
* so only reject it when the value is purely a CPU index.
*/
if (g_ascii_strcasecmp (keys[i], "Processor") == 0 && value[0] != '\0')
{
g_ascii_strtoull (value, &endptr, 10);
if (endptr && endptr != value && *endptr == '\0')
{
g_free (value);
continue;
}
}
if (value[0] != '\0')
{
fclose (fp);
return value;
}
g_free (value);
}
}
}
fclose (fp);
}
if (g_file_test ("/sys/firmware/devicetree/base/model", G_FILE_TEST_IS_REGULAR))
{
char *model = read_trimmed_file ("/sys/firmware/devicetree/base/model");
if (model)
{
return model;
}
}
return g_strdup ("Unknown CPU");
}
static guint
count_physical_cores (void)
{
GDir *dir;
const char *entry;
GHashTable *cores;
guint count;
dir = g_dir_open ("/sys/devices/system/cpu", 0, NULL);
if (!dir)
{
return 0;
}
cores = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
while ((entry = g_dir_read_name (dir)) != NULL)
{
char *package_path;
char *core_path;
char *package_id;
char *core_id;
char *key;
if (!g_str_has_prefix (entry, "cpu") || !g_ascii_isdigit (entry[3]))
{
continue;
}
package_path = g_build_filename ("/sys/devices/system/cpu", entry, "topology", "physical_package_id", NULL);
core_path = g_build_filename ("/sys/devices/system/cpu", entry, "topology", "core_id", NULL);
package_id = read_trimmed_file (package_path);
core_id = read_trimmed_file (core_path);
g_free (package_path);
g_free (core_path);
if (!package_id || !core_id)
{
g_free (package_id);
g_free (core_id);
continue;
}
key = g_strdup_printf ("%s:%s", package_id, core_id);
g_hash_table_add (cores, key);
g_free (package_id);
g_free (core_id);
}
count = g_hash_table_size (cores);
g_hash_table_destroy (cores);
g_dir_close (dir);
return count;
}
static char *
build_cpu_string (void)
{
char *model;
long logical_long;
guint logical;
guint physical;
model = read_cpu_model ();
logical_long = sysconf (_SC_NPROCESSORS_ONLN);
logical = logical_long > 0 ? (guint) logical_long : 0;
physical = count_physical_cores ();
if (physical == 0)
{
physical = logical;
}
if (physical != 0 && logical != 0)
{
char *result = g_strdup_printf ("%s" SYSINFO_SEP "%uC/%uT", model, physical, logical);
g_free (model);
return result;
}
return model;
}
static char *
build_sound_string (void)
{
FILE *fp;
char line[1024];
GPtrArray *cards;
char *result;
fp = fopen ("/proc/asound/cards", "r");
if (!fp)
{
return NULL;
}
cards = g_ptr_array_new_with_free_func (g_free);
while (fgets (line, sizeof (line), fp) != NULL)
{
char *start = line;
char *separator;
char *name;
while (*start && g_ascii_isspace (*start))
{
start++;
}
if (!g_ascii_isdigit (*start))
{
continue;
}
separator = strstr (start, " - ");
if (!separator)
{
continue;
}
name = g_strdup (separator + 3);
g_strstrip (name);
ptr_array_add_unique (cards, name);
g_free (name);
}
fclose (fp);
result = join_ptr_array (cards, ", ");
g_ptr_array_free (cards, TRUE);
return result;
}
#ifdef HAVE_LIBPCI
static char *
resolve_pci_name (struct pci_access *pacc, guint vendor, guint device)
{
char name[1024];
char *resolved;
resolved = pci_lookup_name (pacc, name, sizeof (name),
PCI_LOOKUP_VENDOR | PCI_LOOKUP_DEVICE,
vendor, device);
if (resolved && resolved[0] != '\0')
{
return g_strdup (resolved);
}
return g_strdup_printf ("%04x:%04x", vendor, device);
}
#endif
static char *
clean_gpu_name (const char *raw_name)
{
static const char *vendor_prefixes[] = {
"NVIDIA Corporation ",
"Advanced Micro Devices, Inc. [AMD/ATI] ",
"Advanced Micro Devices, Inc. ",
"Intel Corporation ",
NULL
};
char *name;
char *open;
char *close;
guint i;
if (!raw_name || raw_name[0] == '\0')
return NULL;
name = g_strdup (raw_name);
g_strstrip (name);
/* Prefer the friendly product name at the end of pci.ids entries. */
close = strrchr (name, ']');
open = strrchr (name, '[');
if (open && close && close > open + 1 && close[1] == '\0')
{
char *clean;
*close = '\0';
clean = g_strdup (open + 1);
g_strstrip (clean);
if (clean[0] != '\0')
{
g_free (name);
return clean;
}
g_free (clean);
}
/* Fall back to removing common PCI vendor prefixes. */
for (i = 0; vendor_prefixes[i] != NULL; i++)
{
if (g_str_has_prefix (name, vendor_prefixes[i]))
{
char *clean = g_strdup (name + strlen (vendor_prefixes[i]));
g_strstrip (clean);
if (clean[0] != '\0')
{
g_free (name);
return clean;
}
g_free (clean);
}
}
return name;
}
static void
scan_pci_devices (void)
{
GDir *dir;
const char *entry;
GPtrArray *gpus;
GPtrArray *chipsets;
#ifdef HAVE_LIBPCI
struct pci_access *pacc = NULL;
#endif
snapshot.pci_names = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
gpus = g_ptr_array_new_with_free_func (g_free);
chipsets = g_ptr_array_new_with_free_func (g_free);
#ifdef HAVE_LIBPCI
pacc = pci_alloc ();
if (pacc)
{
pci_init (pacc);
}
#endif
dir = g_dir_open ("/sys/bus/pci/devices", 0, NULL);
if (dir)
{
while ((entry = g_dir_read_name (dir)) != NULL)
{
char *class_path;
char *vendor_path;
char *device_path;
char *name;
char *key;
guint class_code;
guint vendor;
guint device;
guint base_class;
guint class_subclass;
class_path = g_build_filename ("/sys/bus/pci/devices", entry, "class", NULL);
vendor_path = g_build_filename ("/sys/bus/pci/devices", entry, "vendor", NULL);
device_path = g_build_filename ("/sys/bus/pci/devices", entry, "device", NULL);
if (!read_hex_file (class_path, &class_code) ||
!read_hex_file (vendor_path, &vendor) ||
!read_hex_file (device_path, &device))
{
g_free (class_path);
g_free (vendor_path);
g_free (device_path);
continue;
}
g_free (class_path);
g_free (vendor_path);
g_free (device_path);
#ifdef HAVE_LIBPCI
if (pacc)
{
name = resolve_pci_name (pacc, vendor, device);
}
else
#endif
{
name = g_strdup_printf ("%04x:%04x", vendor, device);
}
key = g_strdup_printf ("%04x:%04x", vendor, device);
if (!g_hash_table_contains (snapshot.pci_names, key))
{
g_hash_table_insert (snapshot.pci_names, key, g_strdup (name));
}
else
{
g_free (key);
}
base_class = (class_code >> 16) & 0xff;
class_subclass = (class_code >> 8) & 0xffff;
if (base_class == 0x03)
{
char *gpu_name = clean_gpu_name (name);
if (gpu_name)
{
ptr_array_add_unique (gpus, gpu_name);
g_free (gpu_name);
}
}
else if (class_subclass == 0x0600)
{
ptr_array_add_unique (chipsets, name);
}
g_free (name);
}
g_dir_close (dir);
}
#ifdef HAVE_LIBPCI
if (pacc)
{
pci_cleanup (pacc);
}
#endif
snapshot.gpu = join_ptr_array (gpus, ", ");
snapshot.chipset = join_ptr_array (chipsets, ", ");
g_ptr_array_free (gpus, TRUE);
g_ptr_array_free (chipsets, TRUE);
}
static char *
build_os_string (void)
{
char *pretty;
struct utsname uts;
char *result;
pretty = read_os_pretty_name ();
if (uname (&uts) != 0)
{
return pretty;
}
result = g_strdup_printf ("%s" SYSINFO_SEP "%s %s" SYSINFO_SEP "%s",
pretty, uts.sysname, uts.release, uts.machine);
g_free (pretty);
return result;
}
static void
ensure_os_snapshot (void)
{
if (g_once_init_enter (&os_initialized))
{
snapshot.os = build_os_string ();
g_once_init_leave (&os_initialized, 1);
}
}
static void
ensure_cpu_snapshot (void)
{
if (g_once_init_enter (&cpu_initialized))
{
snapshot.cpu = build_cpu_string ();
g_once_init_leave (&cpu_initialized, 1);
}
}
static void
ensure_pci_snapshot (void)
{
if (g_once_init_enter (&pci_initialized))
{
scan_pci_devices ();
g_once_init_leave (&pci_initialized, 1);
}
}
static void
ensure_sound_snapshot (void)
{
if (g_once_init_enter (&sound_initialized))
{
snapshot.sound = build_sound_string ();
g_once_init_leave (&sound_initialized, 1);
}
}
static char *
format_usage (guint64 total, guint64 available)
{
guint64 used;
char *used_fmt;
char *total_fmt;
char *result;
double percent;
if (total == 0)
{
return NULL;
}
if (available > total)
{
available = total;
}
used = total - available;
percent = ((double) used / (double) total) * 100.0;
used_fmt = g_format_size_full (used, G_FORMAT_SIZE_IEC_UNITS);
total_fmt = g_format_size_full (total, G_FORMAT_SIZE_IEC_UNITS);
result = g_strdup_printf ("%s / %s used (%.0f%%)", used_fmt, total_fmt, percent);
g_free (used_fmt);
g_free (total_fmt);
return result;
}
char *
sysinfo_backend_get_os (void)
{
ensure_os_snapshot ();
return g_strdup (snapshot.os);
}
char *
sysinfo_backend_get_cpu (void)
{
ensure_cpu_snapshot ();
return g_strdup (snapshot.cpu);
}
char *
sysinfo_backend_get_gpu (void)
{
ensure_pci_snapshot ();
return g_strdup (snapshot.gpu);
}
char *
sysinfo_backend_get_chipset (void)
{
ensure_pci_snapshot ();
return g_strdup (snapshot.chipset);
}
char *
sysinfo_backend_get_sound (void)
{
ensure_sound_snapshot ();
return g_strdup (snapshot.sound);
}
char *
sysinfo_backend_get_memory (void)
{
FILE *fp;
char line[512];
guint64 mem_total_kib = 0;
guint64 mem_available_kib = 0;
guint64 swap_total_kib = 0;
guint64 swap_free_kib = 0;
char *memory;
fp = fopen ("/proc/meminfo", "r");
if (!fp)
{
return NULL;
}
while (fgets (line, sizeof (line), fp) != NULL)
{
if (g_str_has_prefix (line, "MemTotal:"))
{
mem_total_kib = g_ascii_strtoull (line + strlen ("MemTotal:"), NULL, 10);
}
else if (g_str_has_prefix (line, "MemAvailable:"))
{
mem_available_kib = g_ascii_strtoull (line + strlen ("MemAvailable:"), NULL, 10);
}
else if (g_str_has_prefix (line, "SwapTotal:"))
{
swap_total_kib = g_ascii_strtoull (line + strlen ("SwapTotal:"), NULL, 10);
}
else if (g_str_has_prefix (line, "SwapFree:"))
{
swap_free_kib = g_ascii_strtoull (line + strlen ("SwapFree:"), NULL, 10);
}
}
fclose (fp);
if (mem_total_kib == 0)
{
return NULL;
}
memory = format_usage (mem_total_kib * 1024, mem_available_kib * 1024);
if (memory && swap_total_kib != 0)
{
char *swap = format_usage (swap_total_kib * 1024, swap_free_kib * 1024);
if (swap)
{
char *result = g_strdup_printf ("%s" SYSINFO_SEP "Swap: %s", memory, swap);
g_free (memory);
g_free (swap);
return result;
}
}
return memory;
}
char *
sysinfo_backend_get_disk (void)
{
const char *path;
struct statvfs fs;
guint64 block_size;
guint64 total;
guint64 available;
char *usage;
path = g_get_home_dir ();
if (!path || path[0] == '\0')
{
path = "/";
}
if (statvfs (path, &fs) != 0)
{
return NULL;
}
block_size = fs.f_frsize != 0 ? (guint64) fs.f_frsize : (guint64) fs.f_bsize;
total = (guint64) fs.f_blocks * block_size;
available = (guint64) fs.f_bavail * block_size;
usage = format_usage (total, available);
return usage;
}
char *
sysinfo_backend_get_uptime (void)
{
char *contents;
char *end = NULL;
double uptime;
contents = read_trimmed_file ("/proc/uptime");
if (!contents)
{
return NULL;
}
uptime = g_ascii_strtod (contents, &end);
if (end == contents || uptime <= 0.0)
{
g_free (contents);
return NULL;
}
g_free (contents);
return sysinfo_format_uptime ((gint64) uptime);
}
static char *
read_driver_name (const char *interface_path)
{
char *driver_path;
char *target;
char *name;
driver_path = g_build_filename (interface_path, "device", "driver", NULL);
target = g_file_read_link (driver_path, NULL);
g_free (driver_path);
if (!target)
{
return NULL;
}
name = g_path_get_basename (target);
g_free (target);
return name;
}
char *
sysinfo_backend_get_network (void)
{
GDir *dir;
const char *entry;
GPtrArray *adapters;
char *result;
ensure_pci_snapshot ();
dir = g_dir_open ("/sys/class/net", 0, NULL);
if (!dir)
{
return NULL;
}
adapters = g_ptr_array_new_with_free_func (g_free);
while ((entry = g_dir_read_name (dir)) != NULL)
{
char *interface_path;
char *device_path;
char *operstate_path;
char *operstate;
char *wireless_path;
char *vendor_path;
char *device_id_path;
char *driver;
const char *hardware_name = NULL;
char *key = NULL;
char *label;
guint vendor;
guint device;
gboolean wireless;
if (strcmp (entry, "lo") == 0)
{
continue;
}
interface_path = g_build_filename ("/sys/class/net", entry, NULL);
device_path = g_build_filename (interface_path, "device", NULL);
if (!g_file_test (device_path, G_FILE_TEST_EXISTS))
{
g_free (device_path);
g_free (interface_path);
continue;
}
operstate_path = g_build_filename (interface_path, "operstate", NULL);
operstate = read_trimmed_file (operstate_path);
g_free (operstate_path);
if (!operstate || g_ascii_strcasecmp (operstate, "up") != 0)
{
g_free (operstate);
g_free (device_path);
g_free (interface_path);
continue;
}
g_free (operstate);
wireless_path = g_build_filename (interface_path, "wireless", NULL);
wireless = g_file_test (wireless_path, G_FILE_TEST_IS_DIR);
g_free (wireless_path);
vendor_path = g_build_filename (device_path, "vendor", NULL);
device_id_path = g_build_filename (device_path, "device", NULL);
if (read_hex_file (vendor_path, &vendor) && read_hex_file (device_id_path, &device))
{
key = g_strdup_printf ("%04x:%04x", vendor, device);
hardware_name = g_hash_table_lookup (snapshot.pci_names, key);
}
g_free (vendor_path);
g_free (device_id_path);
driver = read_driver_name (interface_path);
if (hardware_name)
{
label = g_strdup_printf ("%s: %s (%s)", wireless ? "Wi-Fi" : "Ethernet", hardware_name, entry);
}
else if (driver)
{
label = g_strdup_printf ("%s: %s (%s)", wireless ? "Wi-Fi" : "Ethernet", driver, entry);
}
else
{
label = g_strdup_printf ("%s: %s", wireless ? "Wi-Fi" : "Ethernet", entry);
}
ptr_array_add_unique (adapters, label);
g_free (label);
g_free (driver);
g_free (key);
g_free (device_path);
g_free (interface_path);
}
g_dir_close (dir);
result = join_ptr_array (adapters, ", ");
g_ptr_array_free (adapters, TRUE);
return result;
}
static const char *
sysinfo_detect_toolkit (void)
{
#if defined(USE_GTK_FRONTEND)
return "GTK3";
#else
return NULL;
#endif
}
static const char *
sysinfo_detect_display_backend (void)
{
const char *backend = NULL;
const char *gdk_backend = g_getenv ("GDK_BACKEND");
const char *session = g_getenv ("XDG_SESSION_TYPE");
const gboolean session_wayland = session && g_ascii_strcasecmp (session, "wayland") == 0;
#if defined(USE_GTK_FRONTEND)
{
GdkDisplay *display = gdk_display_get_default ();
if (display)
{
const char *type_name = G_OBJECT_TYPE_NAME (display);
if (type_name)
{
if (g_strrstr (type_name, "Wayland"))
{
backend = "Wayland";
}
else if (g_strrstr (type_name, "X11"))
{
backend = "X11";
}
}
}
}
#endif
if (!backend && gdk_backend)
{
if (g_strrstr (gdk_backend, "wayland"))
{
backend = "Wayland";
}
else if (g_strrstr (gdk_backend, "x11"))
{
backend = "X11";
}
}
if (!backend)
{
const gboolean has_wayland = g_getenv ("WAYLAND_DISPLAY") != NULL;
const gboolean has_x11 = g_getenv ("DISPLAY") != NULL;
if (has_wayland && !has_x11)
{
backend = "Wayland";
}
else if (has_x11 && !has_wayland)
{
backend = "X11";
}
else if (session_wayland)
{
backend = "Wayland";
}
}
if (backend && g_strcmp0 (backend, "X11") == 0 && session_wayland)
{
return "XWayland";
}
return backend;
}
char *
sysinfo_backend_get_ui (void)
{
const char *toolkit = sysinfo_detect_toolkit ();
const char *display = sysinfo_detect_display_backend ();
if (toolkit && display)
{
return g_strdup_printf ("%s / %s", toolkit, display);
}
if (toolkit)
{
return g_strdup (toolkit);
}
if (display)
{
return g_strdup (display);
}
return NULL;
}