Всем привет!

Предыстория (можете пропустить)

Долго думал, писать или не писать статью про мою библиотеку, которой активно пользуюсь и тестирую уже который месяц.

Но недавно обнаружил, что «очередное решение несуществующей проблемы» в виде моего личного Android порта Blender 5.3.0 Alpha, ушло в массы и начало жить своей жизнью, а люди на Reddit выкладывают видосы с тем, как они работают на своих Android планшетах. Причём, мой оригинальный пост собрал еле‑еле 3 стрелочки вверх на Reddit, а пост с видосами да и форк моего репозитория получили дикие отклики по меркам их сабреддитов.

Потому решил, таки рассказать, про самописные вещи, которые лежат в опенсорсе и которыми я активно пользуюсь.

Вступление

Гуглил я как‑то опенсорс C++ библиотеку для мониторинга, температуры и нагрузки компонентов ПК. И не нагуглил. Вот такие вот дела. Есть HWiNFO, платно и закрыто. Есть OpenHardwareMonitor — написан C# и библиотека мертва. Есть форк предыдущей библиотеки — LibreHardwareMonitor — живее всех живых, но C#, да ещё и по дефолту требует 10.NET.

Зачем нужен C++, когда можно подцепить DLL из C# напрямую? Ну вот не хочу я таскать.NET определенной версии с собой из бандла в бандл. AOT компиляция вообще не поможет, тут как раз то самое использование специфических вещей, которые AOT не компилит. Finita la comedia! А ещё есть один косяк у последней библиотеки: мака нет в списке поддержки от слова совсем.

“Одно кольцо, чтоб править всеми” Windows, Linux, MacOS — x64, arm64. Вот тут я и решил сесть и написать её. В очередной раз. Тем более что меня уволили и времени теперь вагон, можно потрогать все свои пэт проджекты. Тем более что есть референс в виде LibreHardwareMonitor.

Правда то, что он есть, хорошо и плохо одновременно. Нельзя сделать, как у них, потому что «лицензия, детка»! Мне их лицензия не нравится. Надо избавлятся, делать надо не так.

Встречайте HardwareMonitorCpp (hardware_monitor_cpp)!

Пытался я получить температуру процессора без повышения привилегий. Гайки, никак. One Ring0 to rule them all! Ну, на винде и макоси, по крайней мере, Ring0 обязателен, Linux может и без этого.

Остальное работает и без sudo. Что такое остальное? Общая информация о железе: как железка называется, сколько у неё частота, память занятая/свободная, текущая температура и текущая нагрузка. Полноценный мониторинг, одним словом. Понятно, что у процессора нет занятой свободной памяти, а у SSD нет текущей частоты, но есть количество активного времени и IO Bound.

Чем классная библиотека?

Ну, во‑первых, к ней сразу идёт Python сервер, которые все эти данные выдает в сеть! Так же есть консольная приложуха, которая выдаёт эти данные визуально в консоль (со своими собственными настройками, котрые сохраняются между запусками). Писать 100 500 утилит не стал. Я пользуюсь только консольной приложухой на винде и маке. Очень нравится, легким движением руки переключаюсь между нагрузкой и температурой (кнопка «t»), а таймскейл на колонку меняю с 1 секунды на 5 минут (кнопки «‑» и «=»). Очень удобно, особенно, чтобы прикинуть визуально, сколько ещё проекту собираться, и сколько он собирался в прошлый раз. И сколько он выжрал оперативы, например.

А ещё на Windows и MacOS наконец‑то есть внятный график и размер файла подкачки. Я сколько не смотрел в TaskManager и ActivityMonitor, так надо найти 2 числа и дальше математикой заниматься. Не круто! Тут сразу видно, что к чему.

Все написано на C++. Потому что я плюсовик и я могу! Консольное приложение в ходе разработки раздулось с 7 мб оперативы до 30 мб на винде. Да и только потому, что я решил больше снапшотов системы хранить в истории отображения.

Из плюшек библиотеки: можно запросить по всему оборудованию только нагрузку или только температуру.

Из минусов: для Windows нужно отдельно ставить PawnIO для цифер процессора, но это нормально. LibreHardwareMontior тоже драйвер таскает и запускатет от имени администратора, для того же самого. Судьба такая. HWiNFO тоже, вот, не просто так просит поднять привелегии.

Что я делать не стал: мониторить температуру материнки и обороты вентилятора. Мне самой температуры железок хватает, чтобы понять, что к чему. А писать код вслепую для 100 500 вендоров не хочется, хоть и есть куда подглядеть.

Консольное приложение и Python сервер

Вместе с самой библиотекой в комплекте идёт консольное приложение и отдельно сервер на Python, который выдаёт эту информацию в сеть.

Несмотря на то, что они могут не покрыть ваши нужды, приложение и сервер — хорошая точка старта. Потом с помощью молотка и напильника можно получить то, что бы вам хотелось. Ну и да, сборка питоновского сервера сразу оборачивает биндинги библиотеки в питоновское расширение, так что можно будет использовать бинарь и в других местах.

Билдится всё очень просто. Всё, что нужно — установленные ninja и cmake, и чтобы они были доступны в Path. Дальше достаточно запустить один из python скриптов сборки. Их три:

  • python build_lib.py

  • python build_console.py

  • python build_server.py

Сборки самодостаточные. После подтягивания саб модулей ничего дополнительно кроме запуска скрипта сборки делать не нужно. В README.md всё подробно описано.

Если впадлу билдить, уже есть готовые сборки релизов (но там не всё).

Примеры использования кода

Вывод температуры ядер:

#include <cstdio>
#include "hardware_monitor_cpp/hardware_monitor_cpp.hpp"

int main() {
    using namespace hardware_monitor_cpp;

    Monitor monitor;
    monitor.addPlatformSources();
    monitor.open();
    Snapshot snap = monitor.poll();

    for (const DeviceInfo& d : snap.devices()) {
        if (d.id.kind != DeviceKind::Cpu) continue;
        std::printf("CPU: %s\n", d.name.c_str());
        for (const Reading& r : snap.forDevice(d.id)) {
            if (r.quantity == Quantity::Temperature)
                std::printf("  %s: %.1f %s\n", r.channel.c_str(), r.value, unitSymbol(r.unit));
        }
    }
    return 0;
}

Монитор загрузки системных ресурсов:

#include <chrono>
#include <cstdio>
#include <string>
#include <thread>
#include <vector>

#include "hardware_monitor_cpp/hardware_monitor_cpp.hpp"

using namespace hardware_monitor_cpp;

static bool isGpu(DeviceKind k)
{
    return k == DeviceKind::GpuIntegrated || k == DeviceKind::GpuDiscrete;
}

static double toGb(double bytes)
{
    return bytes / (1024.0 * 1024.0 * 1024.0);
}

static const Reading* find(const std::vector<Reading>& rs, Quantity q, const std::string& channel)
{
    for (const Reading& r : rs)
    {
        if (r.quantity == q && r.channel == channel)
        {
            return &r;
        }
    }
    return nullptr;
}

static void printStatus(const Snapshot& snap)
{
    for (const DeviceInfo& d : snap.devices())
    {
        std::vector<Reading> rs = snap.forDevice(d.id);

        if (d.id.kind == DeviceKind::Cpu)
        {
            if (const Reading* load = find(rs, Quantity::Load, "Total"))
            {
                std::printf("[CPU] load: %.1f %%\n", load->value);
            }
        }
        else if (isGpu(d.id.kind))
        {
            std::printf("[GPU] %s:\n", d.name.c_str());
            for (const Reading& r : rs)
            {
                if (r.quantity == Quantity::Temperature)
                {
                    std::printf("  - temperature: %.1f %s\n", r.value, unitSymbol(r.unit));
                }
                else if (r.quantity == Quantity::Load && r.channel == "Core")
                {
                    std::printf("  - core load: %.1f %%\n", r.value);
                }
            }
        }
        else if (d.id.kind == DeviceKind::Memory)
        {
            std::printf("[RAM] %s:\n", d.name.c_str());
            if (const Reading* usage = find(rs, Quantity::Load, "Usage"))
            {
                std::printf("  - usage: %.1f %%\n", usage->value);
            }
            const Reading* used = find(rs, Quantity::DataVolume, "Used");
            const Reading* total = find(rs, Quantity::DataVolume, "Total");
            if (used && total)
            {
                std::printf("  - used: %.2f / %.2f GB\n", toGb(used->value), toGb(total->value));
            }
            if (const Reading* swap = find(rs, Quantity::DataVolume, "Swap Used"))
            {
                std::printf("  - swap used: %.2f GB\n", toGb(swap->value));
            }
        }
        else if (d.id.kind == DeviceKind::Storage)
        {
            std::printf("[SSD] %s:\n", d.name.c_str());
            for (const Reading& r : rs)
            {
                if (r.quantity == Quantity::Load && r.channel == "Activity")
                {
                    std::printf("  - activity: %.1f %%\n", r.value);
                }
                else if (r.quantity == Quantity::Level && r.channel == "Used")
                {
                    std::printf("  - space used: %.1f %%\n", r.value);
                }
                else if (r.quantity == Quantity::Temperature)
                {
                    std::printf("  - temperature: %.1f %s\n", r.value, unitSymbol(r.unit));
                }
                else if (r.quantity == Quantity::DataRate)
                {
                    std::printf("  - %s: %.2f MB/s\n", r.channel.c_str(),
                                r.value / (1024.0 * 1024.0));
                }
            }
        }
    }
}

int main()
{
    Monitor monitor;
    monitor.addPlatformSources();
    monitor.open();

    monitor.poll(); // prime the delta-based counters
    for (int i = 0; i < 3; ++i)
    {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        std::printf("=== sample %d ===\n", i + 1);
        printStatus(monitor.poll());
    }
    return 0;
}

Дамп использования оперативной памяти и размера файла подкачки

#include <cstdio>
#include <string>
#include <vector>

#include "hardware_monitor_cpp/hardware_monitor_cpp.hpp"

using namespace hardware_monitor_cpp;

static const Reading* find(const std::vector<Reading>& rs, const std::string& channel) {
    for (const Reading& r : rs)
        if (r.channel == channel) return &r;
    return nullptr;
}

int main() {
    Monitor monitor;
    monitor.addPlatformSources();
    monitor.open();
    Snapshot snap = monitor.poll();

    for (const DeviceInfo& d : snap.devices()) {
        if (d.id.kind != DeviceKind::Memory)
            continue;
        std::printf("Memory: %s [%s]\n", d.name.c_str(), toString(d.id).c_str());

        std::vector<Reading> rs = snap.forDevice(d.id);
        for (const Reading& r : rs)
            std::printf("  %-14s %12.2f %s\n", r.channel.c_str(), r.value, unitSymbol(r.unit));

        if (const Reading* swap = find(rs, "Swap Used"))
            std::printf("  swap in use: %.2f GB\n", swap->value / (1024.0 * 1024.0 * 1024.0));
        else
            std::printf("  swap: not reported on this platform\n");
    }
    return 0;
}

Спасибо за внимание!

Библиотеку можно скачать здесь https://github.com/simfeo/HardwareMonitorCpp или

git clone https://github.com/simfeo/HardwareMonitorCpp.git
cd HardwareMonitorCpp
git submodule update --init --recursive

Как и в прошлой статье про другую мою библиотеку, готов к конструктивной критике, и делать апдейты. После отзывов библиотека, кстати, немного подросла, но серьезно улушилась в синтаксисе использования. Если кому интересно, есть статья на медиум, но пока что на английском, так как большая половина критики прилетела с Reddit.