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
1212namespace caelestia ::services {
1313
1414namespace {
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
27128Gpu::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
43141Gpu::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
79182void 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
167284void 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
193307void 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
224337void 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