1 /*
2 * SPDX-FileCopyrightText: 2014-2016 Sebastian Kügler <sebas@kde.org>
3 *
4 * SPDX-License-Identifier: LGPL-2.1-or-later
5 */
6
7 #include "doctor.h"
8 #include "dpmsclient.h"
9
10 #include <QCollator>
11 #include <QCommandLineParser>
12 #include <QCoreApplication>
13 #include <QDateTime>
14 #include <QFile>
15 #include <QGuiApplication>
16 #include <QJsonArray>
17 #include <QJsonDocument>
18 #include <QJsonObject>
19 #include <QLoggingCategory>
20 #include <QRect>
21 #include <QStandardPaths>
22
23 #include "../backendmanager_p.h"
24 #include "../config.h"
25 #include "../configoperation.h"
26 #include "../edid.h"
27 #include "../getconfigoperation.h"
28 #include "../log.h"
29 #include "../output.h"
30 #include "../setconfigoperation.h"
31
32 Q_LOGGING_CATEGORY(KSCREEN_DOCTOR, "kscreen.doctor")
33
34 static QTextStream cout(stdout);
35 static QTextStream cerr(stderr);
36
37 const static QString green = QStringLiteral("\033[01;32m");
38 const static QString red = QStringLiteral("\033[01;31m");
39 const static QString yellow = QStringLiteral("\033[01;33m");
40 const static QString blue = QStringLiteral("\033[01;34m");
41 const static QString bold = QStringLiteral("\033[01;39m");
42 const static QString cr = QStringLiteral("\033[0;0m");
43
44 namespace KScreen
45 {
46 namespace ConfigSerializer
47 {
48 // Exported private symbol in configserializer_p.h in KScreen
49 extern QJsonObject serializeConfig(const KScreen::ConfigPtr &config);
50 }
51 }
52
53 using namespace KScreen;
54
Doctor(QObject * parent)55 Doctor::Doctor(QObject *parent)
56 : QObject(parent)
57 , m_config(nullptr)
58 , m_changed(false)
59 , m_dpmsClient(nullptr)
60 {
61 }
62
~Doctor()63 Doctor::~Doctor()
64 {
65 }
66
start(QCommandLineParser * parser)67 void Doctor::start(QCommandLineParser *parser)
68 {
69 m_parser = parser;
70 if (m_parser->isSet(QStringLiteral("info"))) {
71 showBackends();
72 }
73 if (parser->isSet(QStringLiteral("json")) || parser->isSet(QStringLiteral("outputs")) || !m_outputArgs.isEmpty()) {
74 KScreen::GetConfigOperation *op = new KScreen::GetConfigOperation();
75 connect(op, &KScreen::GetConfigOperation::finished, this, [this](KScreen::ConfigOperation *op) {
76 configReceived(op);
77 });
78 return;
79 }
80 if (m_parser->isSet(QStringLiteral("dpms"))) {
81 if (!QGuiApplication::platformName().startsWith(QLatin1String("wayland"))) {
82 cerr << "DPMS is only supported on Wayland." << Qt::endl;
83 // We need to kick the event loop, otherwise .quit() hangs
84 QTimer::singleShot(0, qApp->quit);
85 return;
86 }
87
88 m_dpmsClient = new DpmsClient(this);
89 if (m_parser->isSet(QStringLiteral("dpms-excluded"))) {
90 const auto excludedConnectors = m_parser->values(QStringLiteral("dpms-excluded"));
91 m_dpmsClient->setExcludedOutputNames(excludedConnectors);
92 }
93
94 connect(m_dpmsClient, &DpmsClient::finished, qApp, &QCoreApplication::quit);
95
96 const QString dpmsArg = m_parser->value(QStringLiteral("dpms"));
97 if (dpmsArg == QLatin1String("show")) {
98 showDpms();
99 } else {
100 setDpms(dpmsArg);
101 }
102 return;
103 }
104
105 if (m_parser->isSet(QStringLiteral("log"))) {
106 const QString logmsg = m_parser->value(QStringLiteral("log"));
107 if (!Log::instance()->enabled()) {
108 qCWarning(KSCREEN_DOCTOR) << "Logging is disabled, unset KSCREEN_LOGGING in your environment.";
109 } else {
110 Log::log(logmsg);
111 }
112 }
113 // We need to kick the event loop, otherwise .quit() hangs
114 QTimer::singleShot(0, qApp->quit);
115 }
116
setDpms(const QString & dpmsArg)117 void KScreen::Doctor::setDpms(const QString &dpmsArg)
118 {
119 qDebug() << "SetDpms: " << dpmsArg;
120 connect(m_dpmsClient, &DpmsClient::ready, this, [this, dpmsArg]() {
121 cout << "DPMS.ready()";
122 if (dpmsArg == QLatin1String("off")) {
123 m_dpmsClient->off();
124 } else if (dpmsArg == QLatin1String("on")) {
125 m_dpmsClient->on();
126 } else {
127 cout << "--dpms argument not understood (" << dpmsArg << ")";
128 }
129 });
130
131 m_dpmsClient->connect();
132 }
133
showDpms()134 void Doctor::showDpms()
135 {
136 m_dpmsClient = new DpmsClient(this);
137
138 connect(m_dpmsClient, &DpmsClient::ready, this, []() {
139 cout << "DPMS.ready()";
140 });
141
142 m_dpmsClient->connect();
143 }
144
showBackends() const145 void Doctor::showBackends() const
146 {
147 cout << "Environment: " << Qt::endl;
148 auto env_kscreen_backend = qEnvironmentVariable("KSCREEN_BACKEND", QStringLiteral("[not set]"));
149 cout << " * KSCREEN_BACKEND : " << env_kscreen_backend << Qt::endl;
150 auto env_kscreen_backend_inprocess = qEnvironmentVariable("KSCREEN_BACKEND_INPROCESS", QStringLiteral("[not set]"));
151 cout << " * KSCREEN_BACKEND_INPROCESS : " << env_kscreen_backend_inprocess << Qt::endl;
152 auto env_kscreen_logging = qEnvironmentVariable("KSCREEN_LOGGING", QStringLiteral("[not set]"));
153 cout << " * KSCREEN_LOGGING : " << env_kscreen_logging << Qt::endl;
154
155 cout << "Logging to : " << (Log::instance()->enabled() ? Log::instance()->logFile() : QStringLiteral("[logging disabled]")) << Qt::endl;
156 const auto backends = BackendManager::instance()->listBackends();
157 auto preferred = BackendManager::instance()->preferredBackend();
158 cout << "Preferred KScreen backend : " << green << preferred.fileName() << cr << Qt::endl;
159 cout << "Available KScreen backends:" << Qt::endl;
160 for (const QFileInfo &f : backends) {
161 auto c = blue;
162 if (preferred == f) {
163 c = green;
164 }
165 cout << " * " << c << f.fileName() << cr << ": " << f.absoluteFilePath() << Qt::endl;
166 }
167 cout << Qt::endl;
168 }
169
setOptionList(const QStringList & outputArgs)170 void Doctor::setOptionList(const QStringList &outputArgs)
171 {
172 m_outputArgs = outputArgs;
173 }
174
parseOutputArgs()175 void Doctor::parseOutputArgs()
176 {
177 // qCDebug(KSCREEN_DOCTOR) << "POSARGS" << m_positionalArgs;
178 for (const QString &op : qAsConst(m_outputArgs)) {
179 auto ops = op.split(QLatin1Char('.'));
180 if (ops.count() > 2) {
181 bool ok;
182 int output_id = -1;
183 if (ops[0] == QLatin1String("output")) {
184 for (const auto &output : m_config->outputs()) {
185 if (output->name() == ops[1]) {
186 output_id = output->id();
187 }
188 }
189 if (output_id == -1) {
190 output_id = ops[1].toInt(&ok);
191 if (!ok) {
192 cerr << "Unable to parse output id: " << ops[1] << Qt::endl;
193 qApp->exit(3);
194 return;
195 }
196 }
197 if (ops.count() == 3 && ops[2] == QLatin1String("enable")) {
198 if (!setEnabled(output_id, true)) {
199 qApp->exit(1);
200 return;
201 };
202 } else if (ops.count() == 3 && ops[2] == QLatin1String("disable")) {
203 if (!setEnabled(output_id, false)) {
204 qApp->exit(1);
205 return;
206 };
207 } else if (ops.count() == 4 && ops[2] == QLatin1String("mode")) {
208 QString mode_id = ops[3];
209 // set mode
210 if (!setMode(output_id, mode_id)) {
211 qApp->exit(9);
212 return;
213 }
214 qCDebug(KSCREEN_DOCTOR) << "Output" << output_id << "set mode" << mode_id;
215
216 } else if (ops.count() == 4 && ops[2] == QLatin1String("position")) {
217 QStringList _pos = ops[3].split(QLatin1Char(','));
218 if (_pos.count() != 2) {
219 qCWarning(KSCREEN_DOCTOR) << "Invalid position:" << ops[3];
220 qApp->exit(5);
221 return;
222 }
223 int x = _pos[0].toInt(&ok);
224 int y = _pos[1].toInt(&ok);
225 if (!ok) {
226 cerr << "Unable to parse position: " << ops[3] << Qt::endl;
227 qApp->exit(5);
228 return;
229 }
230
231 QPoint p(x, y);
232 qCDebug(KSCREEN_DOCTOR) << "Output position" << p;
233 if (!setPosition(output_id, p)) {
234 qApp->exit(1);
235 return;
236 }
237 } else if ((ops.count() == 4 || ops.count() == 5) && ops[2] == QLatin1String("scale")) {
238 // be lenient about . vs. comma as separator
239 qreal scale = ops[3].replace(QLatin1Char(','), QLatin1Char('.')).toDouble(&ok);
240 if (ops.count() == 5) {
241 const QString dbl = ops[3] + QLatin1String(".") + ops[4];
242 scale = dbl.toDouble(&ok);
243 };
244 // set scale
245 if (!ok || qFuzzyCompare(scale, 0.0) || !setScale(output_id, scale)) {
246 qCDebug(KSCREEN_DOCTOR) << "Could not set scale " << scale << " to output " << output_id;
247 qApp->exit(9);
248 return;
249 }
250 } else if ((ops.count() == 4) && (ops[2] == QLatin1String("orientation") || ops[2] == QStringLiteral("rotation"))) {
251 const QString _rotation = ops[3].toLower();
252 bool ok = false;
253 const QHash<QString, KScreen::Output::Rotation> rotationMap({{QStringLiteral("none"), KScreen::Output::None},
254 {QStringLiteral("normal"), KScreen::Output::None},
255 {QStringLiteral("left"), KScreen::Output::Left},
256 {QStringLiteral("right"), KScreen::Output::Right},
257 {QStringLiteral("inverted"), KScreen::Output::Inverted}});
258 KScreen::Output::Rotation rot = KScreen::Output::None;
259 // set orientation
260 if (rotationMap.contains(_rotation)) {
261 ok = true;
262 rot = rotationMap[_rotation];
263 }
264 if (!ok || !setRotation(output_id, rot)) {
265 qCDebug(KSCREEN_DOCTOR) << "Could not set orientation " << _rotation << " to output " << output_id;
266 qApp->exit(9);
267 return;
268 }
269 } else if (ops.count() == 4 && ops[2] == QLatin1String("overscan")) {
270 const uint32_t overscan = ops[3].toInt();
271 if (overscan > 100) {
272 qCWarning(KSCREEN_DOCTOR) << "Wrong input: allowed values for overscan are from 0 to 100";
273 qApp->exit(9);
274 return;
275 }
276 if (!setOverscan(output_id, overscan)) {
277 qCDebug(KSCREEN_DOCTOR) << "Could not set overscan " << overscan << " to output " << output_id;
278 qApp->exit(9);
279 return;
280 }
281 } else if (ops.count() == 4 && ops[2] == QLatin1String("vrrpolicy")) {
282 const QString _policy = ops[3].toLower();
283 KScreen::Output::VrrPolicy policy;
284 if (_policy == QStringLiteral("never")) {
285 policy = KScreen::Output::VrrPolicy::Never;
286 } else if (_policy == QStringLiteral("always")) {
287 policy = KScreen::Output::VrrPolicy::Always;
288 } else if (_policy == QStringLiteral("automatic")) {
289 policy = KScreen::Output::VrrPolicy::Automatic;
290 } else {
291 qCDebug(KSCREEN_DOCTOR) << "Wrong input: Only allowed values are \"never\", \"always\" and \"automatic\"";
292 qApp->exit(9);
293 return;
294 }
295 if (!setVrrPolicy(output_id, policy)) {
296 qCDebug(KSCREEN_DOCTOR) << "Could not set vrr policy " << policy << " to output " << output_id;
297 qApp->exit(9);
298 return;
299 }
300 } else if (ops.count() == 4 && ops[2] == QLatin1String("rgbrange")) {
301 const QString _range = ops[3].toLower();
302 KScreen::Output::RgbRange range;
303 if (_range == QStringLiteral("automatic")) {
304 range = KScreen::Output::RgbRange::Automatic;
305 } else if (_range == QStringLiteral("full")) {
306 range = KScreen::Output::RgbRange::Full;
307 } else if (_range == QStringLiteral("limited")) {
308 range = KScreen::Output::RgbRange::Limited;
309 } else {
310 qCDebug(KSCREEN_DOCTOR) << "Wrong input: Only allowed values for rgbrange are \"automatic\", \"full\" and \"limited\"";
311 qApp->exit(9);
312 return;
313 }
314 if (!setRgbRange(output_id, range)) {
315 qCDebug(KSCREEN_DOCTOR) << "Could not set rgb range " << range << " to output " << output_id;
316 qApp->exit(9);
317 return;
318 }
319 } else {
320 cerr << "Unable to parse arguments: " << op << Qt::endl;
321 qApp->exit(2);
322 return;
323 }
324 }
325 }
326 }
327 }
328
configReceived(KScreen::ConfigOperation * op)329 void Doctor::configReceived(KScreen::ConfigOperation *op)
330 {
331 m_config = op->config();
332
333 if (m_parser->isSet(QStringLiteral("json"))) {
334 showJson();
335 qApp->quit();
336 }
337 if (m_parser->isSet(QStringLiteral("outputs"))) {
338 showOutputs();
339 qApp->quit();
340 }
341
342 parseOutputArgs();
343
344 if (m_changed) {
345 applyConfig();
346 m_changed = false;
347 }
348 }
349
outputCount() const350 int Doctor::outputCount() const
351 {
352 if (!m_config) {
353 qCWarning(KSCREEN_DOCTOR) << "Invalid config.";
354 return 0;
355 }
356 return m_config->outputs().count();
357 }
358
showOutputs() const359 void Doctor::showOutputs() const
360 {
361 if (!m_config) {
362 qCWarning(KSCREEN_DOCTOR) << "Invalid config.";
363 return;
364 }
365
366 QHash<KScreen::Output::Type, QString> typeString;
367 typeString[KScreen::Output::Unknown] = QStringLiteral("Unknown");
368 typeString[KScreen::Output::VGA] = QStringLiteral("VGA");
369 typeString[KScreen::Output::DVI] = QStringLiteral("DVI");
370 typeString[KScreen::Output::DVII] = QStringLiteral("DVII");
371 typeString[KScreen::Output::DVIA] = QStringLiteral("DVIA");
372 typeString[KScreen::Output::DVID] = QStringLiteral("DVID");
373 typeString[KScreen::Output::HDMI] = QStringLiteral("HDMI");
374 typeString[KScreen::Output::Panel] = QStringLiteral("Panel");
375 typeString[KScreen::Output::TV] = QStringLiteral("TV");
376 typeString[KScreen::Output::TVComposite] = QStringLiteral("TVComposite");
377 typeString[KScreen::Output::TVSVideo] = QStringLiteral("TVSVideo");
378 typeString[KScreen::Output::TVComponent] = QStringLiteral("TVComponent");
379 typeString[KScreen::Output::TVSCART] = QStringLiteral("TVSCART");
380 typeString[KScreen::Output::TVC4] = QStringLiteral("TVC4");
381 typeString[KScreen::Output::DisplayPort] = QStringLiteral("DisplayPort");
382
383 QCollator collator;
384 collator.setNumericMode(true);
385
386 for (const auto &output : m_config->outputs()) {
387 cout << green << "Output: " << cr << output->id() << " " << output->name();
388 cout << " " << (output->isEnabled() ? green + QLatin1String("enabled") : red + QLatin1String("disabled"));
389 cout << " " << (output->isConnected() ? green + QLatin1String("connected") : red + QLatin1String("disconnected"));
390 cout << " " << (output->isPrimary() ? green + QLatin1String("primary") : QString());
391 auto _type = typeString[output->type()];
392 cout << " " << yellow << (_type.isEmpty() ? QStringLiteral("UnmappedOutputType") : _type);
393 cout << blue << " Modes: " << cr;
394
395 const auto modes = output->modes();
396 auto modeKeys = modes.keys();
397 std::sort(modeKeys.begin(), modeKeys.end(), collator);
398
399 for (const auto &key : modeKeys) {
400 auto mode = *modes.find(key);
401
402 auto name = QStringLiteral("%1x%2@%3")
403 .arg(QString::number(mode->size().width()), QString::number(mode->size().height()), QString::number(qRound(mode->refreshRate())));
404 if (mode == output->currentMode()) {
405 name = green + name + QLatin1Char('*') + cr;
406 }
407 if (mode == output->preferredMode()) {
408 name = name + QLatin1Char('!');
409 }
410 cout << mode->id() << ":" << name << " ";
411 }
412 const auto g = output->geometry();
413 cout << yellow << "Geometry: " << cr << g.x() << "," << g.y() << " " << g.width() << "x" << g.height() << " ";
414 cout << yellow << "Scale: " << cr << output->scale() << " ";
415 cout << yellow << "Rotation: " << cr << output->rotation() << " ";
416 cout << yellow << "Overscan: " << cr << output->overscan() << " ";
417 cout << yellow << "Vrr: ";
418 if (output->capabilities() & Output::Capability::Vrr) {
419 switch (output->vrrPolicy()) {
420 case Output::VrrPolicy::Never:
421 cout << cr << "Never ";
422 break;
423 case Output::VrrPolicy::Automatic:
424 cout << cr << "Automatic ";
425 break;
426 case Output::VrrPolicy::Always:
427 cout << cr << "Always ";
428 }
429 } else {
430 cout << cr << "incapable ";
431 }
432 cout << yellow << "RgbRange: ";
433 if (output->capabilities() & Output::Capability::RgbRange) {
434 switch (output->rgbRange()) {
435 case Output::RgbRange::Automatic:
436 cout << cr << "Automatic ";
437 break;
438 case Output::RgbRange::Full:
439 cout << cr << "Full ";
440 break;
441 case Output::RgbRange::Limited:
442 cout << cr << "Limited ";
443 }
444 } else {
445 cout << cr << "unknown ";
446 }
447 if (output->isPrimary()) {
448 cout << blue << "primary";
449 }
450 cout << cr << Qt::endl;
451 }
452 }
453
showJson() const454 void Doctor::showJson() const
455 {
456 QJsonDocument doc(KScreen::ConfigSerializer::serializeConfig(m_config));
457 cout << doc.toJson(QJsonDocument::Indented);
458 }
459
setEnabled(int id,bool enabled=true)460 bool Doctor::setEnabled(int id, bool enabled = true)
461 {
462 if (!m_config) {
463 qCWarning(KSCREEN_DOCTOR) << "Invalid config.";
464 return false;
465 }
466
467 for (const auto &output : m_config->outputs()) {
468 if (output->id() == id) {
469 cout << (enabled ? "Enabling " : "Disabling ") << "output " << id << Qt::endl;
470 output->setEnabled(enabled);
471 m_changed = true;
472 return true;
473 }
474 }
475 cerr << "Output with id " << id << " not found." << Qt::endl;
476 qApp->exit(8);
477 return false;
478 }
479
setPosition(int id,const QPoint & pos)480 bool Doctor::setPosition(int id, const QPoint &pos)
481 {
482 if (!m_config) {
483 qCWarning(KSCREEN_DOCTOR) << "Invalid config.";
484 return false;
485 }
486
487 for (const auto &output : m_config->outputs()) {
488 if (output->id() == id) {
489 qCDebug(KSCREEN_DOCTOR) << "Set output position" << pos;
490 output->setPos(pos);
491 m_changed = true;
492 return true;
493 }
494 }
495 cout << "Output with id " << id << " not found." << Qt::endl;
496 return false;
497 }
498
setMode(int id,const QString & mode_id)499 bool Doctor::setMode(int id, const QString &mode_id)
500 {
501 if (!m_config) {
502 qCWarning(KSCREEN_DOCTOR) << "Invalid config.";
503 return false;
504 }
505
506 for (const auto &output : m_config->outputs()) {
507 if (output->id() == id) {
508 // find mode
509 for (const KScreen::ModePtr mode : output->modes()) {
510 auto name =
511 QStringLiteral("%1x%2@%3")
512 .arg(QString::number(mode->size().width()), QString::number(mode->size().height()), QString::number(qRound(mode->refreshRate())));
513 if (mode->id() == mode_id || name == mode_id) {
514 qCDebug(KSCREEN_DOCTOR) << "Taddaaa! Found mode" << mode->id() << name;
515 output->setCurrentModeId(mode->id());
516 m_changed = true;
517 return true;
518 }
519 }
520 }
521 }
522 cout << "Output mode " << mode_id << " not found." << Qt::endl;
523 return false;
524 }
525
setScale(int id,qreal scale)526 bool Doctor::setScale(int id, qreal scale)
527 {
528 if (!m_config) {
529 qCWarning(KSCREEN_DOCTOR) << "Invalid config.";
530 return false;
531 }
532
533 for (const auto &output : m_config->outputs()) {
534 if (output->id() == id) {
535 output->setScale(scale);
536 m_changed = true;
537 return true;
538 }
539 }
540 cout << "Output scale " << id << " invalid." << Qt::endl;
541 return false;
542 }
543
setRotation(int id,KScreen::Output::Rotation rot)544 bool Doctor::setRotation(int id, KScreen::Output::Rotation rot)
545 {
546 if (!m_config) {
547 qCWarning(KSCREEN_DOCTOR) << "Invalid config.";
548 return false;
549 }
550
551 for (const auto &output : m_config->outputs()) {
552 if (output->id() == id) {
553 output->setRotation(rot);
554 m_changed = true;
555 return true;
556 }
557 }
558 cout << "Output rotation " << id << " invalid." << Qt::endl;
559 return false;
560 }
561
setOverscan(int id,uint32_t overscan)562 bool Doctor::setOverscan(int id, uint32_t overscan)
563 {
564 if (!m_config) {
565 qCWarning(KSCREEN_DOCTOR) << "Invalid config.";
566 return false;
567 }
568
569 for (const auto &output : m_config->outputs()) {
570 if (output->id() == id) {
571 output->setOverscan(overscan);
572 m_changed = true;
573 return true;
574 }
575 }
576 cout << "Output overscan " << id << " invalid." << Qt::endl;
577 return false;
578 }
579
setVrrPolicy(int id,KScreen::Output::VrrPolicy policy)580 bool Doctor::setVrrPolicy(int id, KScreen::Output::VrrPolicy policy)
581 {
582 if (!m_config) {
583 qCWarning(KSCREEN_DOCTOR) << "Invalid config.";
584 return false;
585 }
586
587 for (const auto &output : m_config->outputs()) {
588 if (output->id() == id) {
589 output->setVrrPolicy(policy);
590 m_changed = true;
591 return true;
592 }
593 }
594 cout << "Output VrrPolicy " << id << " invalid." << Qt::endl;
595 return false;
596 }
597
setRgbRange(int id,KScreen::Output::RgbRange rgbRange)598 bool Doctor::setRgbRange(int id, KScreen::Output::RgbRange rgbRange)
599 {
600 if (!m_config) {
601 qCWarning(KSCREEN_DOCTOR) << "Invalid config.";
602 return false;
603 }
604
605 for (const auto &output : m_config->outputs()) {
606 if (output->id() == id) {
607 output->setRgbRange(rgbRange);
608 m_changed = true;
609 return true;
610 }
611 }
612 cout << "Output RgbRange " << id << " invalid." << Qt::endl;
613 return false;
614 }
615
applyConfig()616 void Doctor::applyConfig()
617 {
618 if (!m_changed) {
619 return;
620 }
621 auto setop = new SetConfigOperation(m_config, this);
622 setop->exec();
623 qCDebug(KSCREEN_DOCTOR) << "setop exec returned" << m_config;
624 qApp->exit(0);
625 }
626