fix: fix nvidia update interval on config change + more robust read path for generic
This commit is contained in:
@@ -1,242 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import ZShell.Internal
|
||||
import ZShell.Config
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property real _downloadSpeed: 0
|
||||
property real _downloadTotal: 0
|
||||
|
||||
property real _initialRxBytes: 0
|
||||
property real _initialTxBytes: 0
|
||||
property bool _initialized: false
|
||||
|
||||
property real _prevRxBytes: 0
|
||||
property real _prevTimestamp: 0
|
||||
property real _prevTxBytes: 0
|
||||
property real _uploadSpeed: 0
|
||||
property real _uploadTotal: 0
|
||||
|
||||
readonly property CircularBuffer downloadBuffer: _downloadBuffer
|
||||
|
||||
readonly property real downloadSpeed: _downloadSpeed
|
||||
|
||||
readonly property real downloadTotal: _downloadTotal
|
||||
readonly property int historyLength: Config.dashboard.performance.showVram ? 70 : 30
|
||||
property int refCount: 0
|
||||
readonly property CircularBuffer uploadBuffer: _uploadBuffer
|
||||
readonly property real uploadSpeed: _uploadSpeed
|
||||
readonly property real uploadTotal: _uploadTotal
|
||||
|
||||
function resetSampling(): void {
|
||||
netDevFile.reload();
|
||||
|
||||
const content = netDevFile.text();
|
||||
if (!content)
|
||||
return;
|
||||
|
||||
const data = root.parseNetDev(content);
|
||||
const now = Date.now();
|
||||
|
||||
root._prevRxBytes = data.rx;
|
||||
root._prevTxBytes = data.tx;
|
||||
root._prevTimestamp = now;
|
||||
}
|
||||
|
||||
function formatBytes(bytes: real): var {
|
||||
// Handle negative or invalid values
|
||||
if (bytes < 0 || isNaN(bytes) || !isFinite(bytes)) {
|
||||
return {
|
||||
value: 0,
|
||||
unit: "B/s"
|
||||
};
|
||||
}
|
||||
|
||||
if (bytes < 1024) {
|
||||
return {
|
||||
value: bytes,
|
||||
unit: "B/s"
|
||||
};
|
||||
} else if (bytes < 1024 * 1024) {
|
||||
return {
|
||||
value: bytes / 1024,
|
||||
unit: "KB/s"
|
||||
};
|
||||
} else if (bytes < 1024 * 1024 * 1024) {
|
||||
return {
|
||||
value: bytes / (1024 * 1024),
|
||||
unit: "MB/s"
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
value: bytes / (1024 * 1024 * 1024),
|
||||
unit: "GB/s"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function formatBytesTotal(bytes: real): var {
|
||||
// Handle negative or invalid values
|
||||
if (bytes < 0 || isNaN(bytes) || !isFinite(bytes)) {
|
||||
return {
|
||||
value: 0,
|
||||
unit: "B"
|
||||
};
|
||||
}
|
||||
|
||||
if (bytes < 1024) {
|
||||
return {
|
||||
value: bytes,
|
||||
unit: "B"
|
||||
};
|
||||
} else if (bytes < 1024 * 1024) {
|
||||
return {
|
||||
value: bytes / 1024,
|
||||
unit: "KB"
|
||||
};
|
||||
} else if (bytes < 1024 * 1024 * 1024) {
|
||||
return {
|
||||
value: bytes / (1024 * 1024),
|
||||
unit: "MB"
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
value: bytes / (1024 * 1024 * 1024),
|
||||
unit: "GB"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function parseNetDev(content: string): var {
|
||||
const lines = content.split("\n");
|
||||
let totalRx = 0;
|
||||
let totalTx = 0;
|
||||
|
||||
for (let i = 2; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (!line)
|
||||
continue;
|
||||
|
||||
const parts = line.split(/\s+/);
|
||||
if (parts.length < 10)
|
||||
continue;
|
||||
|
||||
const iface = parts[0].replace(":", "");
|
||||
// Skip loopback interface
|
||||
if (iface === "lo")
|
||||
continue;
|
||||
|
||||
const rxBytes = parseFloat(parts[1]) || 0;
|
||||
const txBytes = parseFloat(parts[9]) || 0;
|
||||
|
||||
totalRx += rxBytes;
|
||||
totalTx += txBytes;
|
||||
}
|
||||
|
||||
return {
|
||||
rx: totalRx,
|
||||
tx: totalTx
|
||||
};
|
||||
}
|
||||
|
||||
CircularBuffer {
|
||||
id: _downloadBuffer
|
||||
|
||||
capacity: root.historyLength + 1
|
||||
}
|
||||
|
||||
CircularBuffer {
|
||||
id: _uploadBuffer
|
||||
|
||||
capacity: root.historyLength + 1
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: netDevFile
|
||||
|
||||
path: "/proc/net/dev"
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: Config.dashboard.resourceUpdateInterval
|
||||
repeat: true
|
||||
running: true
|
||||
triggeredOnStart: true
|
||||
|
||||
onTriggered: {
|
||||
if (root.refCount <= 0) {
|
||||
root.resetSampling();
|
||||
return;
|
||||
}
|
||||
|
||||
netDevFile.reload();
|
||||
const content = netDevFile.text();
|
||||
if (!content)
|
||||
return;
|
||||
|
||||
const data = root.parseNetDev(content);
|
||||
const now = Date.now();
|
||||
|
||||
if (!root._initialized) {
|
||||
root._initialRxBytes = data.rx;
|
||||
root._initialTxBytes = data.tx;
|
||||
root._prevRxBytes = data.rx;
|
||||
root._prevTxBytes = data.tx;
|
||||
root._prevTimestamp = now;
|
||||
root._initialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const timeDelta = (now - root._prevTimestamp) / 1000; // seconds
|
||||
if (timeDelta > 0) {
|
||||
// Calculate byte deltas
|
||||
let rxDelta = data.rx - root._prevRxBytes;
|
||||
let txDelta = data.tx - root._prevTxBytes;
|
||||
|
||||
// Handle counter overflow (when counters wrap around from max to 0)
|
||||
// This happens when counters exceed 32-bit or 64-bit limits
|
||||
if (rxDelta < 0) {
|
||||
// Counter wrapped around - assume 64-bit counter
|
||||
rxDelta += Math.pow(2, 64);
|
||||
}
|
||||
if (txDelta < 0) {
|
||||
txDelta += Math.pow(2, 64);
|
||||
}
|
||||
|
||||
// Calculate speeds
|
||||
root._downloadSpeed = rxDelta / timeDelta;
|
||||
root._uploadSpeed = txDelta / timeDelta;
|
||||
|
||||
if (root._downloadSpeed >= 0 && isFinite(root._downloadSpeed))
|
||||
_downloadBuffer.push(root._downloadSpeed);
|
||||
|
||||
if (root._uploadSpeed >= 0 && isFinite(root._uploadSpeed))
|
||||
_uploadBuffer.push(root._uploadSpeed);
|
||||
}
|
||||
|
||||
// Calculate totals with overflow handling
|
||||
let downTotal = data.rx - root._initialRxBytes;
|
||||
let upTotal = data.tx - root._initialTxBytes;
|
||||
|
||||
// Handle counter overflow for totals
|
||||
if (downTotal < 0) {
|
||||
downTotal += Math.pow(2, 64);
|
||||
}
|
||||
if (upTotal < 0) {
|
||||
upTotal += Math.pow(2, 64);
|
||||
}
|
||||
|
||||
root._downloadTotal = downTotal;
|
||||
root._uploadTotal = upTotal;
|
||||
|
||||
root._prevRxBytes = data.rx;
|
||||
root._prevTxBytes = data.tx;
|
||||
root._prevTimestamp = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,12 +78,12 @@ CustomRect {
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onValuesChanged(): void {
|
||||
function onUpdateTriggered(): void {
|
||||
sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024);
|
||||
slideAnim.restart();
|
||||
}
|
||||
|
||||
target: NetworkUsage.downloadBuffer
|
||||
target: NetworkUsage
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "common.hpp"
|
||||
#include "enums.hpp"
|
||||
|
||||
@@ -20,7 +21,7 @@ class Services : public settings::ObjectNode {
|
||||
CONFIG_PROPERTY(qreal, brightnessIncrement, 0.1)
|
||||
CONFIG_PROPERTY(bool, ddcutilService, true)
|
||||
CONFIG_PROPERTY(QString, defaultPlayer, QStringLiteral("Spotify"))
|
||||
CONFIG_PROPERTY(QString, gpuType, QStringLiteral("NVIDIA"))
|
||||
CONFIG_ENUM_PROPERTY(GpuType, gpuType, GpuType::Auto)
|
||||
CONFIG_PROPERTY(LyricsBackend::Enum, lyricsBackend, LyricsBackend::Auto)
|
||||
CONFIG_PROPERTY(qreal, maxVolume, 1)
|
||||
CONFIG_PROPERTY(qreal, minBrightness, 0.01)
|
||||
|
||||
+342
-248
@@ -2,97 +2,187 @@
|
||||
|
||||
#include "sensorslib.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <qcontainerfwd.h>
|
||||
#include "config/rootnodes.hpp"
|
||||
#include "config/services.hpp"
|
||||
|
||||
#include <qdir.h>
|
||||
#include <qfile.h>
|
||||
#include <qobject.h>
|
||||
#include <qdiriterator.h>
|
||||
#include <qregularexpression.h>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QFileSystemWatcher>
|
||||
#include <QTimer>
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
using Qt::StringLiterals::operator""_s;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kTypeDetectScript =
|
||||
"if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi -L >/dev/null "
|
||||
"2>&1; then echo NVIDIA;"
|
||||
" elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep "
|
||||
"-q .; then echo GENERIC;"
|
||||
" else echo NONE; fi";
|
||||
QStringList cardFiles(const QString& relative) {
|
||||
static const QRegularExpression k_cardRe(u"^card\\d+$"_s);
|
||||
|
||||
constexpr const char* kNameDetectScript =
|
||||
"nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null"
|
||||
" || glxinfo -B 2>/dev/null | grep 'Device:' | cut -d':' -f2 | cut -d'(' "
|
||||
"-f1"
|
||||
" || lspci 2>/dev/null | grep -i 'vga\\|3d controller\\|display' | head -1";
|
||||
QStringList files;
|
||||
QDirIterator it(u"/sys/class/drm"_s, QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
while (it.hasNext()) {
|
||||
const QString path = it.next();
|
||||
if (!k_cardRe.match(it.fileName()).hasMatch()) continue;
|
||||
const QString file = path + relative;
|
||||
if (QFile::exists(file)) files << file;
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
std::optional<qreal> readNumber(const QString& path) {
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) return std::nullopt;
|
||||
bool ok = false;
|
||||
const qreal v = f.readAll().trimmed().toDouble(&ok);
|
||||
if (!ok) return std::nullopt;
|
||||
return v;
|
||||
}
|
||||
|
||||
QString cleanName(QString s) {
|
||||
static const QRegularExpression k_noise(
|
||||
u"\\(R\\)|\\(TM\\)|Graphics"_s,
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
static const QRegularExpression k_spaces(u"\\s+"_s);
|
||||
s.replace(k_noise, {});
|
||||
s.replace(k_spaces, u" "_s);
|
||||
return s.trimmed();
|
||||
}
|
||||
|
||||
QString parseNvidiaName(const QByteArray& out) {
|
||||
const QString first =
|
||||
QString::fromUtf8(out).split(u'\n').value(0).trimmed();
|
||||
return first.isEmpty() ? QString() : cleanName(first);
|
||||
}
|
||||
|
||||
QString parseGlxinfoName(const QByteArray& out) {
|
||||
const QStringList lines = QString::fromUtf8(out).split(u'\n');
|
||||
for (const QString& line : lines) {
|
||||
const qsizetype idx = line.indexOf(u"Device:"_s);
|
||||
if (idx < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString rest = line.mid(idx + 7);
|
||||
const qsizetype paren = rest.indexOf(u'(');
|
||||
if (paren >= 0) {
|
||||
rest = rest.left(paren);
|
||||
}
|
||||
|
||||
const QString cleaned = cleanName(rest);
|
||||
if (!cleaned.isEmpty()) {
|
||||
return cleaned;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
QString parseLspciName(const QByteArray& out) {
|
||||
static const QRegularExpression k_lineRe(
|
||||
u"vga|3d controller|display"_s,
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
|
||||
const QStringList lines = QString::fromUtf8(out).split(u'\n');
|
||||
QString match;
|
||||
for (const QString& line : lines) {
|
||||
if (k_lineRe.match(line).hasMatch()) {
|
||||
match = line;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (match.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
static const QRegularExpression k_bracketRe(u"\\[([^\\]]+)\\][^\\[]*$"_s);
|
||||
const auto bracket = k_bracketRe.match(match);
|
||||
if (bracket.hasMatch()) {
|
||||
return cleanName(bracket.captured(1));
|
||||
}
|
||||
|
||||
static const QRegularExpression k_colonRe(u":\\s+(.+)"_s);
|
||||
const auto colon = k_colonRe.match(match);
|
||||
if (colon.hasMatch()) {
|
||||
return cleanName(colon.captured(1));
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
struct NameSource {
|
||||
QString program;
|
||||
QStringList args;
|
||||
QString (*parse)(const QByteArray&);
|
||||
};
|
||||
|
||||
const std::array<NameSource, 3>& nameSources() {
|
||||
static const std::array<NameSource, 3> k_sources = {{
|
||||
{
|
||||
.program = u"nvidia-smi"_s,
|
||||
.args = {u"--query-gpu=name"_s, u"--format=csv,noheader"_s},
|
||||
.parse = &parseNvidiaName,
|
||||
},
|
||||
{
|
||||
.program = u"glxinfo"_s,
|
||||
.args = {u"-B"_s},
|
||||
.parse = &parseGlxinfoName,
|
||||
},
|
||||
{
|
||||
.program = u"lspci"_s,
|
||||
.args = {},
|
||||
.parse = &parseLspciName,
|
||||
},
|
||||
}};
|
||||
return k_sources;
|
||||
}
|
||||
|
||||
constexpr int k_nvidiaSource = 0;
|
||||
constexpr int k_firstGenericSource = 1;
|
||||
|
||||
} // namespace
|
||||
|
||||
Gpu::Gpu(QObject* parent) : TickingService(parent) {
|
||||
QString configPath =
|
||||
QDir::homePath() + QStringLiteral("/.config/zshell/config.json");
|
||||
auto* svc = config::ConfigSingleton::instance()->services();
|
||||
m_userType = svc->gpuType();
|
||||
QObject::connect(svc, &config::Services::gpuTypeChanged, this, [this, svc] {
|
||||
const GpuType value = svc->gpuType();
|
||||
if (value == m_userType) return;
|
||||
m_userType = value;
|
||||
resolveGpu();
|
||||
});
|
||||
|
||||
auto reloadConfig = [this, configPath]() {
|
||||
QFile file(configPath);
|
||||
if (file.open(QIODevice::ReadOnly)) {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
|
||||
if (!doc.isNull()) {
|
||||
QJsonObject services =
|
||||
doc.object().value("services").toObject();
|
||||
setUserType(parseType(services.value("gpuType").toString()));
|
||||
}
|
||||
}
|
||||
};
|
||||
resolveGpu();
|
||||
|
||||
reloadConfig();
|
||||
m_restartTimer = new QTimer(this);
|
||||
m_restartTimer->setSingleShot(true);
|
||||
m_restartTimer->setInterval(500);
|
||||
QObject::connect(m_restartTimer, &QTimer::timeout, this, [this] {
|
||||
if (!m_nvidiaProc || m_type != GpuType::Nvidia) return;
|
||||
|
||||
static QFileSystemWatcher* watcher = new QFileSystemWatcher();
|
||||
if (!watcher->files().contains(configPath)) {
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QFileSystemWatcher::fileChanged,
|
||||
this,
|
||||
[this, configPath]() {
|
||||
QTimer::singleShot(100, this, [this, configPath]() {
|
||||
QFile file(configPath);
|
||||
if (file.open(QIODevice::ReadOnly)) {
|
||||
QJsonDocument doc =
|
||||
QJsonDocument::fromJson(file.readAll());
|
||||
if (!doc.isNull()) {
|
||||
QJsonObject services =
|
||||
doc.object().value("services").toObject();
|
||||
setUserType(parseType(
|
||||
services.value("gpuType").toString()));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
watcher->addPath(configPath);
|
||||
stopNvidiaUsage();
|
||||
startNvidiaUsage();
|
||||
});
|
||||
|
||||
QObject::connect(this, &TickingService::updateIntervalChanged, this, [this] {
|
||||
if (!m_nvidiaProc) return;
|
||||
m_restartTimer->start();
|
||||
});
|
||||
}
|
||||
|
||||
Gpu::~Gpu() {
|
||||
if (!m_nvidiaProc) return;
|
||||
m_nvidiaProc->disconnect(this);
|
||||
m_nvidiaProc->terminate();
|
||||
if (!m_nvidiaProc->waitForFinished(500)) {
|
||||
m_nvidiaProc->kill();
|
||||
m_nvidiaProc->waitForFinished(200);
|
||||
}
|
||||
|
||||
// Detection must run before any ServiceRef appears: callers may gate the ref on
|
||||
// `type !== Gpu.None`, which would otherwise deadlock the detection.
|
||||
if (m_userType == Auto) {
|
||||
detectTypeOnce();
|
||||
}
|
||||
detectNameOnce();
|
||||
}
|
||||
|
||||
Gpu::Type Gpu::type() const {
|
||||
return m_userType == Auto ? m_autoType : m_userType;
|
||||
}
|
||||
|
||||
Gpu::Type Gpu::userType() const {
|
||||
return m_userType;
|
||||
}
|
||||
|
||||
Gpu::Type Gpu::autoType() const {
|
||||
return m_autoType;
|
||||
GpuType Gpu::type() const {
|
||||
return m_type;
|
||||
}
|
||||
|
||||
QString Gpu::name() const {
|
||||
@@ -115,28 +205,13 @@ qreal Gpu::memoryTotal() const {
|
||||
return m_memoryTotal;
|
||||
}
|
||||
|
||||
void Gpu::setUserType(Type value) {
|
||||
if (value == m_userType) {
|
||||
return;
|
||||
}
|
||||
const Type prevDerived = type();
|
||||
m_userType = value;
|
||||
Q_EMIT userTypeChanged();
|
||||
if (type() != prevDerived) {
|
||||
Q_EMIT typeChanged();
|
||||
}
|
||||
}
|
||||
void Gpu::setType(GpuType value) {
|
||||
if (value == m_type) return;
|
||||
|
||||
void Gpu::setAutoType(Type value) {
|
||||
if (value == m_userType) {
|
||||
return;
|
||||
}
|
||||
const Type prevDerived = type();
|
||||
m_autoType = value;
|
||||
Q_EMIT autoTypeChanged();
|
||||
if (type() != prevDerived) {
|
||||
Q_EMIT typeChanged();
|
||||
}
|
||||
m_type = value;
|
||||
if (m_type != GpuType::Nvidia) stopNvidiaUsage();
|
||||
if (m_type == GpuType::None) resetUsage();
|
||||
emit typeChanged();
|
||||
}
|
||||
|
||||
void Gpu::setName(QString value) {
|
||||
@@ -147,6 +222,12 @@ void Gpu::setName(QString value) {
|
||||
Q_EMIT nameChanged();
|
||||
}
|
||||
|
||||
void Gpu::setDetecting(bool value) {
|
||||
if (value == m_detecting) return;
|
||||
m_detecting = value;
|
||||
emit detectingChanged();
|
||||
}
|
||||
|
||||
void Gpu::setMemoryUsed(qreal value) {
|
||||
if (std::abs(m_memoryUsed - value) < 0.001) return;
|
||||
m_memoryUsed = value;
|
||||
@@ -160,168 +241,192 @@ void Gpu::setMemoryTotal(qreal value) {
|
||||
}
|
||||
|
||||
void Gpu::tick() {
|
||||
const Type t = type();
|
||||
if (t == Generic) {
|
||||
if (m_type == GpuType::Generic) {
|
||||
readGenericUsage();
|
||||
readGenericMemory();
|
||||
readGpuTemperature();
|
||||
} else if (t == Nvidia) {
|
||||
} else if (m_type == GpuType::Nvidia) {
|
||||
startNvidiaUsage();
|
||||
} else {
|
||||
if (std::abs(m_percentage) > 0.0001) {
|
||||
m_percentage = 0.0;
|
||||
Q_EMIT percentageChanged();
|
||||
}
|
||||
if (std::abs(m_temperature) > 0.05) {
|
||||
m_temperature = 0.0;
|
||||
Q_EMIT temperatureChanged();
|
||||
}
|
||||
resetUsage();
|
||||
}
|
||||
}
|
||||
|
||||
void Gpu::detectTypeOnce() {
|
||||
if (m_typeProc) {
|
||||
return;
|
||||
void Gpu::refreshSysfsCache() {
|
||||
m_busyFiles = cardFiles(u"/device/gpu_busy_percent"_s);
|
||||
|
||||
m_vramFiles.clear();
|
||||
const QStringList totals = cardFiles(u"/device/mem_info_vram_total"_s);
|
||||
for (const QString& total : totals) {
|
||||
QString used = total;
|
||||
used.replace(u"mem_info_vram_total"_s, u"mem_info_vram_used"_s);
|
||||
if (QFile::exists(used)) m_vramFiles.append({used, total});
|
||||
}
|
||||
m_typeProc = new QProcess(this);
|
||||
QObject::connect(
|
||||
m_typeProc,
|
||||
&QProcess::finished,
|
||||
this,
|
||||
[this](int, QProcess::ExitStatus) {
|
||||
const QByteArray out =
|
||||
m_typeProc->readAllStandardOutput().trimmed();
|
||||
if (!out.isEmpty()) {
|
||||
setAutoType(parseType(QString::fromLatin1(out)));
|
||||
}
|
||||
m_typeProc->deleteLater();
|
||||
m_typeProc = nullptr;
|
||||
});
|
||||
m_typeProc->start(
|
||||
QStringLiteral("sh"),
|
||||
{QStringLiteral("-c"), QString::fromLatin1(kTypeDetectScript)});
|
||||
}
|
||||
|
||||
void Gpu::detectNameOnce() {
|
||||
if (m_nameProc) {
|
||||
void Gpu::resolveGpu() {
|
||||
refreshSysfsCache();
|
||||
const int generation = ++m_generation;
|
||||
|
||||
if (m_userType != GpuType::Auto) setType(m_userType);
|
||||
|
||||
if (m_userType == GpuType::None) {
|
||||
setName({});
|
||||
setDetecting(false);
|
||||
return;
|
||||
}
|
||||
m_nameProc = new QProcess(this);
|
||||
|
||||
setName({});
|
||||
setDetecting(true);
|
||||
tryNameSource(
|
||||
m_userType == GpuType::Generic ? k_firstGenericSource : k_nvidiaSource,
|
||||
generation);
|
||||
}
|
||||
|
||||
int Gpu::probeEnd() const {
|
||||
return m_userType == GpuType::Nvidia
|
||||
? k_firstGenericSource
|
||||
: static_cast<int>(nameSources().size());
|
||||
}
|
||||
|
||||
void Gpu::tryNameSource(int index, int generation) {
|
||||
const NameSource& src = nameSources().at(static_cast<std::size_t>(index));
|
||||
runProcess(
|
||||
src.program,
|
||||
src.args,
|
||||
[this, index, generation, parse = src.parse](const QByteArray& out) {
|
||||
finishNameSource(index, generation, parse(out));
|
||||
});
|
||||
}
|
||||
|
||||
void Gpu::finishNameSource(int index, int generation, QString name) {
|
||||
if (generation != m_generation) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_userType == GpuType::Auto && index == k_nvidiaSource) {
|
||||
if (!name.isEmpty())
|
||||
setType(GpuType::Nvidia);
|
||||
else
|
||||
setType(m_busyFiles.isEmpty() ? GpuType::None : GpuType::Generic);
|
||||
|
||||
if (m_type == GpuType::None) {
|
||||
setName({});
|
||||
setDetecting(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!name.isEmpty()) {
|
||||
setName(std::move(name));
|
||||
setDetecting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const int next = index + 1;
|
||||
if (next < probeEnd()) {
|
||||
tryNameSource(next, generation);
|
||||
} else {
|
||||
setName({});
|
||||
setDetecting(false);
|
||||
}
|
||||
}
|
||||
|
||||
void Gpu::runProcess(
|
||||
const QString& program,
|
||||
const QStringList& args,
|
||||
std::function<void(const QByteArray&)> callback) {
|
||||
auto* proc = new QProcess(this);
|
||||
proc->setStandardErrorFile(QProcess::nullDevice());
|
||||
|
||||
const auto finish =
|
||||
[proc, callback = std::move(callback)](const QByteArray& out) {
|
||||
callback(out);
|
||||
proc->deleteLater();
|
||||
};
|
||||
|
||||
QObject::connect(
|
||||
m_nameProc,
|
||||
proc,
|
||||
&QProcess::finished,
|
||||
this,
|
||||
[this](int, QProcess::ExitStatus) {
|
||||
const QString output =
|
||||
QString::fromUtf8(m_nameProc->readAllStandardOutput()).trimmed();
|
||||
if (!output.isEmpty()) {
|
||||
const QString lower = output.toLower();
|
||||
if (lower.contains(QStringLiteral("nvidia")) ||
|
||||
lower.contains(QStringLiteral("geforce")) ||
|
||||
lower.contains(QStringLiteral("rtx")) ||
|
||||
lower.contains(QStringLiteral("gtx")) ||
|
||||
lower.contains(QStringLiteral("rx"))) {
|
||||
setName(cleanName(output));
|
||||
} else {
|
||||
static const QRegularExpression bracketRe(
|
||||
QStringLiteral("\\[([^\\]]+)\\][^\\[]*$"));
|
||||
const auto bracket = bracketRe.match(output);
|
||||
if (bracket.hasMatch()) {
|
||||
setName(cleanName(bracket.captured(1)));
|
||||
} else {
|
||||
static const QRegularExpression colonRe(
|
||||
QStringLiteral(":\\s*(.+)"));
|
||||
const auto colon = colonRe.match(output);
|
||||
if (colon.hasMatch()) {
|
||||
setName(cleanName(colon.captured(1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
m_nameProc->deleteLater();
|
||||
m_nameProc = nullptr;
|
||||
[finish, proc](int code, QProcess::ExitStatus status) {
|
||||
const bool ok = status == QProcess::NormalExit && code == 0;
|
||||
finish(ok ? proc->readAllStandardOutput() : QByteArray());
|
||||
});
|
||||
m_nameProc->start(
|
||||
QStringLiteral("sh"),
|
||||
{QStringLiteral("-c"), QString::fromLatin1(kNameDetectScript)});
|
||||
QObject::connect(
|
||||
proc,
|
||||
&QProcess::errorOccurred,
|
||||
this,
|
||||
[finish](QProcess::ProcessError err) {
|
||||
if (err == QProcess::FailedToStart) {
|
||||
finish(QByteArray());
|
||||
}
|
||||
});
|
||||
|
||||
proc->start(program, args);
|
||||
}
|
||||
|
||||
void Gpu::onStopped() {
|
||||
stopNvidiaUsage();
|
||||
}
|
||||
|
||||
void Gpu::readGenericMemory() {
|
||||
const QStringList paths = QDir(QStringLiteral("/sys/class/drm"))
|
||||
.entryList(
|
||||
QStringList() << QStringLiteral("card*"),
|
||||
QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
|
||||
qreal totalMem = 0.0;
|
||||
qreal usedMem = 0.0;
|
||||
for (const QString& card : paths) {
|
||||
QFile total(
|
||||
QStringLiteral("/sys/class/drm/%1/device/mem_info_vram_total")
|
||||
.arg(card));
|
||||
if (!total.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
continue;
|
||||
}
|
||||
bool ok = false;
|
||||
const qreal v = total.readAll().trimmed().toDouble(&ok);
|
||||
total.close();
|
||||
if (ok) {
|
||||
totalMem += v;
|
||||
}
|
||||
|
||||
QFile used(QStringLiteral("/sys/class/drm/%1/device/mem_info_vram_used")
|
||||
.arg(card));
|
||||
if (!used.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
continue;
|
||||
}
|
||||
bool ok1 = false;
|
||||
const qreal v1 = used.readAll().trimmed().toDouble(&ok1);
|
||||
used.close();
|
||||
if (ok1) {
|
||||
usedMem += v1;
|
||||
}
|
||||
qreal total = 0.0;
|
||||
qreal used = 0.0;
|
||||
for (const VramFiles& v : std::as_const(m_vramFiles)) {
|
||||
const auto t = readNumber(v.total);
|
||||
const auto u = readNumber(v.used);
|
||||
if (!t || !u) continue;
|
||||
total += *t;
|
||||
used += *u;
|
||||
}
|
||||
|
||||
setMemoryTotal(totalMem);
|
||||
setMemoryUsed(usedMem);
|
||||
setMemoryTotal(total);
|
||||
setMemoryUsed(used);
|
||||
}
|
||||
|
||||
void Gpu::readGenericUsage() {
|
||||
const QStringList paths = QDir(QStringLiteral("/sys/class/drm"))
|
||||
.entryList(
|
||||
QStringList() << QStringLiteral("card*"),
|
||||
QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
qreal sum = 0.0;
|
||||
int count = 0;
|
||||
for (const QString& card : paths) {
|
||||
QFile f(QStringLiteral("/sys/class/drm/%1/device/gpu_busy_percent")
|
||||
.arg(card));
|
||||
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
continue;
|
||||
}
|
||||
bool ok = false;
|
||||
const qreal v = f.readAll().trimmed().toDouble(&ok);
|
||||
f.close();
|
||||
if (ok) {
|
||||
sum += v;
|
||||
for (const QString& path : std::as_const(m_busyFiles)) {
|
||||
if (const auto v = readNumber(path)) {
|
||||
sum += *v;
|
||||
++count;
|
||||
}
|
||||
}
|
||||
const qreal newPerc = count > 0 ? sum / count / 100.0 : 0.0;
|
||||
if (std::abs(newPerc - m_percentage) > 0.0001) {
|
||||
m_percentage = newPerc;
|
||||
Q_EMIT percentageChanged();
|
||||
emit percentageChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void Gpu::startNvidiaUsage() {
|
||||
if (m_nvidiaProc) {
|
||||
void Gpu::stopNvidiaUsage() {
|
||||
m_restartTimer->stop();
|
||||
|
||||
if (!m_nvidiaProc) return;
|
||||
|
||||
QProcess* proc = m_nvidiaProc;
|
||||
m_nvidiaProc = nullptr;
|
||||
|
||||
proc->disconnect(this);
|
||||
if (proc->state() == QProcess::NotRunning) {
|
||||
proc->deleteLater();
|
||||
return;
|
||||
}
|
||||
m_nvidiaProc = new QProcess(this);
|
||||
QObject::connect(proc, &QProcess::finished, proc, &QObject::deleteLater);
|
||||
proc->terminate();
|
||||
}
|
||||
|
||||
void Gpu::startNvidiaUsage() {
|
||||
if (m_nvidiaProc) return;
|
||||
|
||||
auto* proc = new QProcess(this);
|
||||
m_nvidiaProc = proc;
|
||||
|
||||
QObject::connect(
|
||||
m_nvidiaProc, &QProcess::readyReadStandardOutput, this, [this]() {
|
||||
while (m_nvidiaProc->canReadLine()) {
|
||||
const QByteArray out = m_nvidiaProc->readLine();
|
||||
proc, &QProcess::readyReadStandardOutput, this, [this, proc]() {
|
||||
while (proc->canReadLine()) {
|
||||
const QByteArray out = proc->readLine();
|
||||
const QString output = QString::fromUtf8(out).trimmed();
|
||||
if (output.isEmpty()) continue;
|
||||
|
||||
@@ -357,21 +462,21 @@ void Gpu::startNvidiaUsage() {
|
||||
}
|
||||
});
|
||||
|
||||
QObject::connect(m_nvidiaProc, &QProcess::finished, this, [this]() {
|
||||
m_nvidiaProc->deleteLater();
|
||||
m_nvidiaProc = nullptr;
|
||||
});
|
||||
auto cleanup = [this, proc]() {
|
||||
if (m_nvidiaProc == proc) m_nvidiaProc = nullptr;
|
||||
|
||||
proc->deleteLater();
|
||||
};
|
||||
|
||||
QObject::connect(proc, &QProcess::finished, this, cleanup);
|
||||
|
||||
QObject::connect(
|
||||
m_nvidiaProc,
|
||||
proc,
|
||||
&QProcess::errorOccurred,
|
||||
this,
|
||||
[this](QProcess::ProcessError) {
|
||||
m_nvidiaProc->deleteLater();
|
||||
m_nvidiaProc = nullptr;
|
||||
});
|
||||
[cleanup](QProcess::ProcessError) { cleanup(); });
|
||||
|
||||
m_nvidiaProc->start(
|
||||
proc->start(
|
||||
QStringLiteral("nvidia-smi"),
|
||||
{QStringLiteral(
|
||||
"--query-gpu=utilization.gpu,temperature.gpu,"
|
||||
@@ -386,32 +491,21 @@ void Gpu::readGpuTemperature() {
|
||||
const qreal newTemp = t.value_or(0.0);
|
||||
if (std::abs(newTemp - m_temperature) > 0.05) {
|
||||
m_temperature = newTemp;
|
||||
Q_EMIT temperatureChanged();
|
||||
emit temperatureChanged();
|
||||
}
|
||||
}
|
||||
|
||||
Gpu::Type Gpu::parseType(const QString& s) {
|
||||
const QString u = s.trimmed().toUpper();
|
||||
if (u.isEmpty()) {
|
||||
return Auto;
|
||||
void Gpu::resetUsage() {
|
||||
if (std::abs(m_percentage) > 0.0001) {
|
||||
m_percentage = 0.0;
|
||||
emit percentageChanged();
|
||||
}
|
||||
if (u == QStringLiteral("NVIDIA")) {
|
||||
return Nvidia;
|
||||
if (std::abs(m_temperature) > 0.05) {
|
||||
m_temperature = 0.0;
|
||||
emit temperatureChanged();
|
||||
}
|
||||
if (u == QStringLiteral("GENERIC")) {
|
||||
return Generic;
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
QString Gpu::cleanName(QString s) {
|
||||
static const QRegularExpression noise(
|
||||
QStringLiteral("\\(R\\)|\\(TM\\)|Graphics"),
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
static const QRegularExpression spaces(QStringLiteral("\\s+"));
|
||||
s.replace(noise, QString());
|
||||
s.replace(spaces, QStringLiteral(" "));
|
||||
return s.trimmed();
|
||||
setMemoryUsed(0.0);
|
||||
setMemoryTotal(0.0);
|
||||
}
|
||||
|
||||
} // namespace ZShell::services
|
||||
|
||||
@@ -1,31 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include "tickingservice.hpp"
|
||||
#include "config/enums.hpp"
|
||||
|
||||
#include <qprocess.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
using GpuType = config::GpuType::Enum;
|
||||
|
||||
struct VramFiles {
|
||||
QString used;
|
||||
QString total;
|
||||
};
|
||||
|
||||
class Gpu : public TickingService {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
public:
|
||||
enum Type {
|
||||
Auto, // user override is empty (config "") — defer to detected autoType
|
||||
None, // no usable GPU
|
||||
Nvidia, // queried via nvidia-smi
|
||||
Generic, // queried via /sys/class/drm/card*/device/gpu_busy_percent
|
||||
};
|
||||
Q_ENUM(Type)
|
||||
|
||||
private:
|
||||
Q_PROPERTY(Type type READ type NOTIFY typeChanged)
|
||||
Q_PROPERTY(Type userType READ userType NOTIFY userTypeChanged)
|
||||
Q_PROPERTY(Type autoType READ autoType NOTIFY autoTypeChanged)
|
||||
Q_PROPERTY(GpuType type READ type NOTIFY typeChanged)
|
||||
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
|
||||
Q_PROPERTY(bool detecting READ detecting NOTIFY detectingChanged)
|
||||
Q_PROPERTY(qreal percentage READ percentage NOTIFY percentageChanged)
|
||||
Q_PROPERTY(qreal temperature READ temperature NOTIFY temperatureChanged)
|
||||
Q_PROPERTY(qreal memoryUsed READ memoryUsed NOTIFY memoryUsedChanged)
|
||||
@@ -33,11 +31,11 @@ class Gpu : public TickingService {
|
||||
|
||||
public:
|
||||
explicit Gpu(QObject* parent = nullptr);
|
||||
~Gpu() override;
|
||||
|
||||
[[nodiscard]] Type type() const;
|
||||
[[nodiscard]] Type userType() const;
|
||||
[[nodiscard]] Type autoType() const;
|
||||
[[nodiscard]] GpuType type() const;
|
||||
[[nodiscard]] QString name() const;
|
||||
[[nodiscard]] bool detecting() const;
|
||||
[[nodiscard]] qreal percentage() const;
|
||||
[[nodiscard]] qreal temperature() const;
|
||||
[[nodiscard]] qreal memoryUsed() const;
|
||||
@@ -45,9 +43,8 @@ class Gpu : public TickingService {
|
||||
|
||||
signals:
|
||||
void typeChanged();
|
||||
void userTypeChanged();
|
||||
void autoTypeChanged();
|
||||
void nameChanged();
|
||||
void detectingChanged();
|
||||
void percentageChanged();
|
||||
void temperatureChanged();
|
||||
void memoryUsedChanged();
|
||||
@@ -55,35 +52,52 @@ class Gpu : public TickingService {
|
||||
|
||||
protected:
|
||||
void tick() override;
|
||||
void onStopped() override;
|
||||
|
||||
private:
|
||||
void detectTypeOnce();
|
||||
void detectNameOnce();
|
||||
void resolveGpu();
|
||||
|
||||
[[nodiscard]] int probeEnd() const;
|
||||
|
||||
void tryNameSource(int index, int generation);
|
||||
void finishNameSource(int index, int generation, QString name);
|
||||
|
||||
void readGenericUsage();
|
||||
void stopNvidiaUsage();
|
||||
void startNvidiaUsage();
|
||||
void readGpuTemperature();
|
||||
void readGenericMemory();
|
||||
void resetUsage();
|
||||
|
||||
void setUserType(Type value);
|
||||
void setAutoType(Type value);
|
||||
void runProcess(
|
||||
const QString& program,
|
||||
const QStringList& args,
|
||||
std::function<void(const QByteArray&)> callback);
|
||||
|
||||
void setType(GpuType value);
|
||||
void setName(QString value);
|
||||
void setDetecting(bool value);
|
||||
void setMemoryUsed(qreal value);
|
||||
void setMemoryTotal(qreal value);
|
||||
|
||||
[[nodiscard]] static Type parseType(const QString& s);
|
||||
[[nodiscard]] static QString cleanName(QString s);
|
||||
void refreshSysfsCache();
|
||||
|
||||
Type m_userType = Auto;
|
||||
Type m_autoType = None;
|
||||
GpuType m_userType = GpuType::Auto;
|
||||
GpuType m_type = GpuType::None;
|
||||
QString m_name;
|
||||
bool m_detecting = false;
|
||||
qreal m_percentage = 0.0;
|
||||
qreal m_temperature = 0.0;
|
||||
qreal m_memoryUsed = 0.0;
|
||||
qreal m_memoryTotal = 0.0;
|
||||
|
||||
QProcess* m_typeProc = nullptr;
|
||||
QProcess* m_nameProc = nullptr;
|
||||
QStringList m_busyFiles;
|
||||
QList<VramFiles> m_vramFiles;
|
||||
int m_generation = 0;
|
||||
bool m_nvidiaQuerying = false;
|
||||
|
||||
QProcess* m_nvidiaProc = nullptr;
|
||||
QTimer* m_restartTimer = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ZShell::services
|
||||
|
||||
@@ -37,6 +37,7 @@ void TickingService::start() {
|
||||
void TickingService::stop() {
|
||||
m_running = false;
|
||||
m_timer->stop();
|
||||
onStopped();
|
||||
}
|
||||
|
||||
void TickingService::applyInterval(int ms) {
|
||||
|
||||
@@ -26,6 +26,7 @@ class TickingService : public Service {
|
||||
void stop() final;
|
||||
|
||||
virtual void tick() = 0;
|
||||
virtual void onStopped() {}
|
||||
|
||||
private:
|
||||
void applyInterval(int ms);
|
||||
|
||||
Reference in New Issue
Block a user