Skip to content

Commit ebb0172

Browse files
committed
feat: remove chain procs for gpu detection
Use C++ native read of gpu files + nvidia proc for type detection Nvidia proc also doubles for name detection Closes caelestia-dots#1615
1 parent c24a024 commit ebb0172

2 files changed

Lines changed: 209 additions & 98 deletions

File tree

plugin/src/Caelestia/Services/gpu.cpp

Lines changed: 196 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,138 @@
44
#include "../Config/serviceconfig.hpp"
55
#include "sensorslib.hpp"
66

7-
#include <cmath>
87
#include <qdir.h>
8+
#include <qdiriterator.h>
99
#include <qfile.h>
1010
#include <qregularexpression.h>
1111

1212
namespace caelestia::services {
1313

1414
namespace {
1515

16-
constexpr const char* kTypeDetectScript =
17-
"if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi -L >/dev/null 2>&1; then echo NVIDIA;"
18-
" elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC;"
19-
" else echo NONE; fi";
16+
QStringList gpuBusyFiles() {
17+
static const QRegularExpression cardRe(QStringLiteral("^card\\d+$"));
2018

21-
constexpr const char* kNameDetectScript = "nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null"
22-
" || glxinfo -B 2>/dev/null | grep 'Device:' | cut -d':' -f2 | cut -d'(' -f1"
23-
" || lspci 2>/dev/null | grep -i 'vga\\|3d controller\\|display' | head -1";
19+
QStringList files;
20+
QDirIterator it(QStringLiteral("/sys/class/drm"), QDir::Dirs | QDir::NoDotAndDotDot);
21+
while (it.hasNext()) {
22+
const QString path = it.next();
23+
if (!cardRe.match(it.fileName()).hasMatch()) {
24+
continue;
25+
}
26+
const QString busy = path + QStringLiteral("/device/gpu_busy_percent");
27+
if (QFile::exists(busy)) {
28+
files << busy;
29+
}
30+
}
31+
return files;
32+
}
33+
34+
QString cleanName(QString s) {
35+
static const QRegularExpression noise(
36+
QStringLiteral("\\(R\\)|\\(TM\\)|Graphics"), QRegularExpression::CaseInsensitiveOption);
37+
static const QRegularExpression spaces(QStringLiteral("\\s+"));
38+
s.replace(noise, QString());
39+
s.replace(spaces, QStringLiteral(" "));
40+
return s.trimmed();
41+
}
42+
43+
QString parseNvidiaName(const QByteArray& out) {
44+
const QString first = QString::fromUtf8(out).split('\n').value(0).trimmed();
45+
return first.isEmpty() ? QString() : cleanName(first);
46+
}
47+
48+
QString parseGlxinfoName(const QByteArray& out) {
49+
const QStringList lines = QString::fromUtf8(out).split('\n');
50+
for (const QString& line : lines) {
51+
const qsizetype idx = line.indexOf(QStringLiteral("Device:"));
52+
if (idx < 0) {
53+
continue;
54+
}
55+
56+
QString rest = line.mid(idx + 7);
57+
const qsizetype paren = rest.indexOf('(');
58+
if (paren >= 0) {
59+
rest = rest.left(paren);
60+
}
61+
62+
const QString cleaned = cleanName(rest);
63+
if (!cleaned.isEmpty()) {
64+
return cleaned;
65+
}
66+
}
67+
68+
return QString();
69+
}
70+
71+
QString parseLspciName(const QByteArray& out) {
72+
static const QRegularExpression lineRe(
73+
QStringLiteral("vga|3d controller|display"), QRegularExpression::CaseInsensitiveOption);
74+
75+
const QStringList lines = QString::fromUtf8(out).split('\n');
76+
QString match;
77+
for (const QString& line : lines) {
78+
if (lineRe.match(line).hasMatch()) {
79+
match = line;
80+
break;
81+
}
82+
}
83+
84+
if (match.isEmpty()) {
85+
return QString();
86+
}
87+
88+
static const QRegularExpression bracketRe(QStringLiteral("\\[([^\\]]+)\\][^\\[]*$"));
89+
const auto bracket = bracketRe.match(match);
90+
if (bracket.hasMatch()) {
91+
return cleanName(bracket.captured(1));
92+
}
93+
94+
// Split on a colon followed by whitespace so the PCI slot ("00:02.0") is not
95+
// mistaken for the class/name separator ("controller: Device").
96+
static const QRegularExpression colonRe(QStringLiteral(":\\s+(.+)"));
97+
const auto colon = colonRe.match(match);
98+
if (colon.hasMatch()) {
99+
return cleanName(colon.captured(1));
100+
}
101+
102+
return QString();
103+
}
104+
105+
struct NameSource {
106+
QString program;
107+
QStringList args;
108+
QString (*parse)(const QByteArray&);
109+
};
110+
111+
// Name probes in priority order; the first non-empty result wins. The NVIDIA
112+
// probe is first and doubles as the type probe (see finishNameSource).
113+
const std::array<NameSource, 3>& nameSources() {
114+
static const std::array<NameSource, 3> sources = { {
115+
{ QStringLiteral("nvidia-smi"), { QStringLiteral("--query-gpu=name"), QStringLiteral("--format=csv,noheader") },
116+
&parseNvidiaName },
117+
{ QStringLiteral("glxinfo"), { QStringLiteral("-B") }, &parseGlxinfoName },
118+
{ QStringLiteral("lspci"), {}, &parseLspciName },
119+
} };
120+
return sources;
121+
}
122+
123+
// Index of the NVIDIA source within nameSources(); its result also drives type.
124+
constexpr int kNvidiaSource = 0;
24125

25126
} // namespace
26127

27128
Gpu::Gpu(QObject* parent)
28129
: TickingService(parent) {
130+
m_busyFiles = gpuBusyFiles();
131+
29132
auto* svc = caelestia::config::GlobalConfig::instance()->services();
30133
m_userType = parseType(svc->gpuType());
31134
QObject::connect(svc, &caelestia::config::ServiceConfig::gpuTypeChanged, this, [this, svc] {
32135
setUserType(parseType(svc->gpuType()));
33136
});
34137

35-
// Detection must run before any ServiceRef appears: callers may gate the ref on
36-
// `type !== Gpu.None`, which would otherwise deadlock the detection.
37-
if (m_userType == Auto) {
38-
detectTypeOnce();
39-
}
40-
detectNameOnce();
138+
detectGpu();
41139
}
42140

43141
Gpu::Type Gpu::type() const {
@@ -74,6 +172,11 @@ void Gpu::setUserType(Type value) {
74172
if (type() != prevDerived) {
75173
Q_EMIT typeChanged();
76174
}
175+
176+
// Probe again when switching back to auto
177+
if (value == Auto) {
178+
detectGpu();
179+
}
77180
}
78181

79182
void Gpu::setAutoType(Type value) {
@@ -115,63 +218,74 @@ void Gpu::tick() {
115218
}
116219
}
117220

118-
void Gpu::detectTypeOnce() {
119-
if (m_typeProc) {
221+
void Gpu::detectGpu() {
222+
if (m_detecting) {
120223
return;
121224
}
122-
m_typeProc = new QProcess(this);
123-
QObject::connect(m_typeProc, &QProcess::finished, this, [this](int, QProcess::ExitStatus) {
124-
const QByteArray out = m_typeProc->readAllStandardOutput().trimmed();
125-
if (!out.isEmpty()) {
126-
setAutoType(parseType(QString::fromLatin1(out)));
127-
}
128-
m_typeProc->deleteLater();
129-
m_typeProc = nullptr;
225+
m_detecting = true;
226+
227+
// Probe in priority order, stopping at the first result
228+
tryNameSource(0);
229+
}
230+
231+
void Gpu::tryNameSource(int index) {
232+
const NameSource& src = nameSources().at(static_cast<std::size_t>(index));
233+
runProcess(src.program, src.args, [this, index, parse = src.parse](const QByteArray& out) {
234+
finishNameSource(index, parse(out));
130235
});
131-
m_typeProc->start(QStringLiteral("sh"), { QStringLiteral("-c"), QString::fromLatin1(kTypeDetectScript) });
132236
}
133237

134-
void Gpu::detectNameOnce() {
135-
if (m_nameProc) {
238+
void Gpu::finishNameSource(int index, QString name) {
239+
// The NVIDIA name probe doubles as the type probe: a non-empty result means an
240+
// NVIDIA GPU is present and queryable. Derive autoType unconditionally (even when
241+
// the user pins a type) so a later switch to Auto reads a correct value without
242+
// depending on its own re-probe, which is skipped while a probe is in flight.
243+
if (index == kNvidiaSource) {
244+
setAutoType(!name.isEmpty() ? Nvidia : (m_busyFiles.isEmpty() ? None : Generic));
245+
}
246+
247+
if (!name.isEmpty()) {
248+
setName(std::move(name));
249+
m_detecting = false;
136250
return;
137251
}
138-
m_nameProc = new QProcess(this);
139-
QObject::connect(m_nameProc, &QProcess::finished, this, [this](int, QProcess::ExitStatus) {
140-
const QString output = QString::fromUtf8(m_nameProc->readAllStandardOutput()).trimmed();
141-
if (!output.isEmpty()) {
142-
const QString lower = output.toLower();
143-
if (lower.contains(QStringLiteral("nvidia")) || lower.contains(QStringLiteral("geforce")) ||
144-
lower.contains(QStringLiteral("rtx")) || lower.contains(QStringLiteral("gtx")) ||
145-
lower.contains(QStringLiteral("rx"))) {
146-
setName(cleanName(output));
147-
} else {
148-
static const QRegularExpression bracketRe(QStringLiteral("\\[([^\\]]+)\\][^\\[]*$"));
149-
const auto bracket = bracketRe.match(output);
150-
if (bracket.hasMatch()) {
151-
setName(cleanName(bracket.captured(1)));
152-
} else {
153-
static const QRegularExpression colonRe(QStringLiteral(":\\s*(.+)"));
154-
const auto colon = colonRe.match(output);
155-
if (colon.hasMatch()) {
156-
setName(cleanName(colon.captured(1)));
157-
}
158-
}
159-
}
252+
253+
if (index + 1 < static_cast<int>(nameSources().size())) {
254+
tryNameSource(index + 1);
255+
} else {
256+
m_detecting = false;
257+
}
258+
}
259+
260+
void Gpu::runProcess(const QString& program, const QStringList& args, std::function<void(const QByteArray&)> callback) {
261+
auto* proc = new QProcess(this);
262+
proc->setStandardErrorFile(QProcess::nullDevice());
263+
264+
// Deliver the result exactly once, then tear the process down. A crash or a
265+
// missing binary yields empty output so the caller can fall through gracefully:
266+
// only FailedToStart skips finished(), and a crash reports CrashExit there.
267+
const auto finish = [proc, callback = std::move(callback)](const QByteArray& out) {
268+
callback(out);
269+
proc->deleteLater();
270+
};
271+
272+
QObject::connect(proc, &QProcess::finished, this, [finish, proc](int, QProcess::ExitStatus status) {
273+
finish(status == QProcess::NormalExit ? proc->readAllStandardOutput() : QByteArray());
274+
});
275+
QObject::connect(proc, &QProcess::errorOccurred, this, [finish](QProcess::ProcessError err) {
276+
if (err == QProcess::FailedToStart) {
277+
finish(QByteArray());
160278
}
161-
m_nameProc->deleteLater();
162-
m_nameProc = nullptr;
163279
});
164-
m_nameProc->start(QStringLiteral("sh"), { QStringLiteral("-c"), QString::fromLatin1(kNameDetectScript) });
280+
281+
proc->start(program, args);
165282
}
166283

167284
void Gpu::readGenericUsage() {
168-
const QStringList paths =
169-
QDir(QStringLiteral("/sys/class/drm"))
170-
.entryList(QStringList() << QStringLiteral("card*"), QDir::Dirs | QDir::NoDotAndDotDot);
171285
qreal sum = 0.0;
172286
int count = 0;
173-
for (const QString& card : paths) {
174-
QFile f(QStringLiteral("/sys/class/drm/%1/device/gpu_busy_percent").arg(card));
287+
for (const QString& path : std::as_const(m_busyFiles)) {
288+
QFile f(path);
175289
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
176290
continue;
177291
}
@@ -191,34 +305,33 @@ void Gpu::readGenericUsage() {
191305
}
192306

193307
void Gpu::startNvidiaUsage() {
194-
if (m_nvidiaProc) {
308+
if (m_nvidiaQuerying) {
195309
return;
196310
}
197-
m_nvidiaProc = new QProcess(this);
198-
QObject::connect(m_nvidiaProc, &QProcess::finished, this, [this](int, QProcess::ExitStatus) {
199-
const QByteArray out = m_nvidiaProc->readAllStandardOutput().trimmed();
200-
m_nvidiaProc->deleteLater();
201-
m_nvidiaProc = nullptr;
202-
203-
const QList<QByteArray> parts = out.split(',');
204-
if (parts.size() < 2) {
205-
return;
206-
}
207-
bool ok1 = false;
208-
bool ok2 = false;
209-
const qreal usage = parts.at(0).trimmed().toDouble(&ok1) / 100.0;
210-
const qreal temp = parts.at(1).trimmed().toDouble(&ok2);
211-
if (ok1 && std::abs(usage - m_percentage) > 0.0001) {
212-
m_percentage = usage;
213-
Q_EMIT percentageChanged();
214-
}
215-
if (ok2 && std::abs(temp - m_temperature) > 0.05) {
216-
m_temperature = temp;
217-
Q_EMIT temperatureChanged();
218-
}
219-
});
220-
m_nvidiaProc->start(QStringLiteral("nvidia-smi"), { QStringLiteral("--query-gpu=utilization.gpu,temperature.gpu"),
221-
QStringLiteral("--format=csv,noheader,nounits") });
311+
m_nvidiaQuerying = true;
312+
runProcess(QStringLiteral("nvidia-smi"),
313+
{ QStringLiteral("--query-gpu=utilization.gpu,temperature.gpu"),
314+
QStringLiteral("--format=csv,noheader,nounits") },
315+
[this](const QByteArray& out) {
316+
m_nvidiaQuerying = false;
317+
318+
const QList<QByteArray> parts = out.trimmed().split(',');
319+
if (parts.size() < 2) {
320+
return;
321+
}
322+
bool ok1 = false;
323+
bool ok2 = false;
324+
const qreal usage = parts.at(0).trimmed().toDouble(&ok1) / 100.0;
325+
const qreal temp = parts.at(1).trimmed().toDouble(&ok2);
326+
if (ok1 && std::abs(usage - m_percentage) > 0.0001) {
327+
m_percentage = usage;
328+
Q_EMIT percentageChanged();
329+
}
330+
if (ok2 && std::abs(temp - m_temperature) > 0.05) {
331+
m_temperature = temp;
332+
Q_EMIT temperatureChanged();
333+
}
334+
});
222335
}
223336

224337
void Gpu::readGpuTemperature() {
@@ -244,13 +357,4 @@ Gpu::Type Gpu::parseType(const QString& s) {
244357
return None;
245358
}
246359

247-
QString Gpu::cleanName(QString s) {
248-
static const QRegularExpression noise(
249-
QStringLiteral("\\(R\\)|\\(TM\\)|Graphics"), QRegularExpression::CaseInsensitiveOption);
250-
static const QRegularExpression spaces(QStringLiteral("\\s+"));
251-
s.replace(noise, QString());
252-
s.replace(spaces, QStringLiteral(" "));
253-
return s.trimmed();
254-
}
255-
256360
} // namespace caelestia::services

0 commit comments

Comments
 (0)