Разбираем исходный код системного оптимизатора Stacer на Qt/C++. Узнайте, как устроена архитектура, работа с /proc, привилегии и темизация. Примените лучшие практики в своих проектах.
Вы учите Qt по учебникам, где строят калькуляторы и туду-листы? Они учат API, но не показывают, как устроено реальное приложение. Мы возьмём готовый проект — Stacer, Linux-оптимизатор, — и разберём его код. Вы узнаете, как организовать архитектуру, работать с системой, управлять привилегиями и делать темы. После прочтения вы сможете применять эти приёмы в своих приложениях.
Stacer заброшен — последний коммит в августе 2023, 678 коммитов и тишина. Это идеально для изучения: код не меняется, можно спокойно всё прочитать. В проекте около 50 заголовочных файлов, 48 исходников и 26 .ui-форм — хватит на вечер.
Первым делом смотрим CMakeLists.txt. Верхний уровень:
add_subdirectory(stacer-core)
add_subdirectory(stacer)В stacer-core подключаются только Qt5 Core и Network — без Widgets, без Charts. Это статическая библиотека, которая не может рисовать пиксели. Всё, что связано с GUI, лежит в stacer, которое линкуется с Widgets, Charts, Svg, Concurrent.
Такое разделение — редкость. Обычно в проектах обещают разделение, а потом в data-слое появляется QMessageBox. Здесь линковщик не даст этого сделать. stacer-core делится на три части по глаголам:
Читать, писать, инфраструктура. Никогда не приходится гадать, где что лежит.
Stacer читает текстовые файлы из /proc. Например:
#define PROC_CPUINFO "/proc/cpuinfo"
#define PROC_LOADAVG "/proc/loadavg"
#define PROC_STAT "/proc/stat"Количество ядер: прочитать /proc/cpuinfo, отфильтровать строки с ^processor, посчитать. Средняя загрузка: прочитать /proc/loadavg, разделить по пробелам, взять первые три числа. Частота: отфильтровать ^cpu MHz, взять число после двоеточия.
Всё это — регулярки над текстовыми файлами. procfs — стандартный интерфейс Linux.
/proc/stat выдаёт счётчики тиков с момента загрузки: user, system, idle, iowait и т.д. Они только растут. CPU usage — это не значение, а отношение между двумя замерами. Из cpu_info.cpp:
int CpuInfo::getCpuPercent(const QList<double>& cpuTimes, const int& processor) const
{
const int N = getCpuCoreCount() + 1;
static QVector<double> l_idles(N);
static QVector<double> l_totals(N);
int utilisation = 0;
if (cpuTimes.count() > 0)
{
double idle = cpuTimes.at(3) + cpuTimes.at(4); // idle + iowait
double total = 0.0;
for (const double& t : cpuTimes) total += t;
double idle_delta = idle - l_idles[processor];
double total_delta = total - l_totals[processor];
if (total_delta)
utilisation = 100 * ((total_delta - idle_delta) / total_delta);
l_idles[processor] = idle;
l_totals[processor] = total;
}
return utilisation;
}Функция запоминает предыдущие значения в статических переменных. Это значит, что она stateful: если вызвать её два раза подряд из разных мест, второй вызов получит мусор. Первый вызов после старта всегда бессмыслен. Но это правильный алгоритм — так работает top.
Таймер обновления — из dashboard_page.cpp:
connect(mTimer, &QTimer::timeout, this, &DashboardPage::updateCpuBar);
connect(mTimer, &QTimer::timeout, this, &DashboardPage::updateMemoryBar);
connect(mTimer, &QTimer::timeout, this, &DashboardPage::updateNetworkBar);
QTimer *timerDisk = new QTimer(this);
connect(timerDisk, &QTimer::timeout, this, &DashboardPage::updateDiskBar);
timerDisk->start(5 * 1000);
mTimer->start(1 * 1000);Один таймер на 1 секунду разветвляется на три слота, второй на 5 секунд — для диска, потому что диск меняется медленно. Никакого push, UI сам дёргает данные по таймеру.
Stacer может убивать процессы, чистить кеши, управлять systemd, удалять пакеты. Всё это требует root. Но Stacer не запускается от root. Из command_util.cpp:
QString CommandUtil::sudoExec(const QString& cmd, QStringList args, QByteArray data)
{
args.push_front(cmd);
QString result("");
try {
result = CommandUtil::exec("pkexec", args, data);
} catch (QString& ex) {
qCritical() << ex;
}
return result;
}Команда запускается через pkexec. polkit показывает диалог авторизации, и только одна команда выполняется с правами root. Приложение остаётся непривилегированным — баг в коде темы не сможет удалить вашу домашнюю папку. Привилегия выдаётся на каждую операцию отдельно.
Всего 16 вызовов sudoExec. Альтернатива — запускать весь GUI под sudo, но тогда event loop и парсер стилей будут root-процессами.
В Tools/package_tool.cpp та же идея: команды отправляются в apt-get, dnf, yum, pacman или snap в зависимости от дистрибутива.
Каждая страница — папка в Pages/ с тремя файлами: .h, .cpp, .ui. При старте приложение создаёт их все и помещает в SlidingStackedWidget — наследник QStackedWidget с анимацией перехода. Это мелочь, но она делает интерфейс приятнее.
Две страницы создаются условно:
if (ToolManager::ins()->checkSourceRepository()) {
aptSourceManagerPage = new APTSourceManagerPage(mSlidingStacked);
mListPages.insert(7, aptSourceManagerPage);
mListSidebarButtons.insert(7, ui->btnAptSourceManager);
} else {
ui->btnAptSourceManager->hide();
}На Arch нет APT, поэтому страница не создаётся, кнопка скрывается. Аналогично для Gnome — проверяется DESKTOP_SESSION и дистрибутив.
QSS похож на CSS, но в нём нет переменных. Stacer сделал их сам. Каждая тема — папка со style.qss и values.ini. AppManager::updateStylesheet делает так:
QString appThemePath = QString(":/static/themes/%1/style").arg(mSettingManager->getThemeName());
mStyleValues = new QSettings(QString("%1/values.ini").arg(appThemePath), QSettings::IniFormat);
mStylesheetFileContent = FileUtil::readStringFromFile(QString("%1/style.qss").arg(appThemePath));
for (const QString& key : mStyleValues->allKeys()) {
mStylesheetFileContent.replace(key, mStyleValues->value(key).toString());
}
qApp->setStyleSheet(mStylesheetFileContent);
emit SignalMapper::ins()->sigChangedAppTheme();Читаем стили, заменяем все @color01 на #fff из ini, применяем. Смена темы — указать другую папку.
Виджеты подключаются к стилям через setAccessibleName("danger"), а QSS выбирает по этому имени. Это Qt-приём: поле доступности используется как CSS-класс. И в конце — сигнал SignalMapper::ins()->sigChangedAppTheme(). SignalMapper — глобальный QObject-шина сигналов. Три сигнала:
signals:
void sigChangedAppTheme();
void sigUninstallStarted();
void sigUninstallFinished();Виджеты, которые сами рисуют (круговые диаграммы), не могут получать цвета из QSS, поэтому им нужно знать, когда тема сменилась. Без шины AppManager пришлось бы хранить указатели на все такие виджеты. С шиной — крик в пустоту, и кто слушает, тот реагирует.
Если хотите проследить сквозной пример: процент CPU. Таймер в dashboard_page.cpp, через InfoManager::getCpuPercents, в CpuInfo::getCpuPercents, который читает /proc/stat, вычисляет дельту и отдаёт число в кастомный CircleBar. Весь стек приложения в одном числе, раз в секунду.
Прямо сейчас откройте любой свой Qt-проект и проверьте: можно ли собрать ядро без GUI? Есть ли в нём QMessageBox? Если да — вынесите системный слой в отдельную статическую библиотеку. Это займёт час, но сэкономит дни отладки.
А если хотите глубже разобраться — форкните Stacer и попробуйте добавить новую страницу. Или напишите своё приложение с SlidingStackedWidget. Делитесь опытом в комментариях!
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →