File indexing completed on 2024-04-21 05:45:52

0001 // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0002 // SPDX-FileCopyrightText: 2021 Harald Sitter <sitter@kde.org>
0003 
0004 #include <QApplication>
0005 #include <QQmlApplicationEngine>
0006 #include <QIcon>
0007 #include <QQmlContext>
0008 #include <QStandardPaths>
0009 #include <QFile>
0010 #include <QWindow>
0011 #include <KStatusNotifierItem>
0012 #include <KDeclarative/KDeclarative>
0013 #include <KLocalizedContext>
0014 #include <KLocalizedString>
0015 #include <KAuthAction>
0016 #include <KAuthExecuteJob>
0017 #include <KJob>
0018 #include <KOSRelease>
0019 
0020 class AuthHelper : public QObject
0021 {
0022     Q_OBJECT
0023     // Does the grub config for nomodeset exist (if not disable() will not be able to do anything)
0024     Q_PROPERTY(bool grubCfgExists MEMBER m_grubCfgExists CONSTANT)
0025     // Is a disable() call currently running
0026     Q_PROPERTY(bool busy MEMBER m_busy NOTIFY busyChanged)
0027     // Was nomodeset disabled (via disable() call)
0028     Q_PROPERTY(bool disabled MEMBER m_disabled NOTIFY busyChanged)
0029     // Last (fatal) error. Empty when no error.
0030     Q_PROPERTY(QString error MEMBER m_error NOTIFY errorChanged)
0031     // Is system running as a live session from an ISO or the like.
0032     Q_PROPERTY(bool liveSession MEMBER m_liveSession CONSTANT)
0033     // True when installing the live session results in nomodeset as well (e.g. configured in grub)
0034     Q_PROPERTY(bool livePersistent MEMBER m_livePersistent CONSTANT)
0035 public:
0036     using QObject::QObject;
0037 
0038     Q_INVOKABLE void disable()
0039     {
0040         if (m_busy) {
0041             return;
0042         }
0043         setBusy(true);
0044 
0045         KAuth::Action action(QStringLiteral("org.kde.nomodeset.disable"));
0046         action.setHelperId(QStringLiteral("org.kde.nomodeset"));
0047 
0048         qDebug() << action.isValid() << action.hasHelper() << action.helperId() << action.status();
0049         KAuth::ExecuteJob *job = action.execute();
0050         connect(job, &KJob::result, this, [job, this] {
0051             qDebug() << job->error() << job->errorString() << job->errorText();
0052             switch (static_cast<KAuth::ActionReply::Error>(job->error())) {
0053             case KAuth::ActionReply::NoError:
0054                 m_disabled = true;
0055                 writeIgnore();
0056                 break;
0057             case KAuth::ActionReply::AuthorizationDeniedError:
0058                 Q_FALLTHROUGH();
0059             case KAuth::ActionReply::UserCancelledError:
0060                 // leave disabled state alone -> falls back to original page in gui
0061                 break;
0062             default:
0063                 m_error = job->errorString();
0064                 if (m_error.isEmpty()) {
0065                     m_error = job->errorText();
0066                 }
0067                 if (m_error.isEmpty()) {
0068                     m_error = i18nc("@info:status", "Unknown error code: %1", QString::number(job->error()));
0069                 }
0070                 Q_EMIT errorChanged();
0071                 break;
0072             }
0073 
0074             setBusy(false);
0075         });
0076         job->start();
0077     }
0078 
0079     bool shouldIgnore()
0080     {
0081         return !QStandardPaths::locate(QStandardPaths::TempLocation, ignoreMaker()).isEmpty();
0082     }
0083 
0084     void writeIgnore()
0085     {
0086         const QString path =
0087             QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QLatin1Char('/') + ignoreMaker();
0088         QFile file(path);
0089         file.open(QIODevice::WriteOnly);
0090     }
0091 
0092 Q_SIGNALS:
0093     void busyChanged();
0094     void disablingChanged();
0095     void errorChanged();
0096 
0097 private:
0098     static QString ignoreMaker()
0099     {
0100         return QStringLiteral(".kde-nomodeset-ignore");
0101     }
0102 
0103     // BLOCKING on helper
0104     bool grubCfgExists()
0105     {
0106         KAuth::Action action(QStringLiteral("org.kde.nomodeset.grubcfgexists"));
0107         action.setHelperId(QStringLiteral("org.kde.nomodeset"));
0108 
0109         qDebug() << action.isValid() << action.hasHelper() << action.helperId() << action.status();
0110         KAuth::ExecuteJob *job = action.execute();
0111         bool result = false;
0112         connect(job, &KJob::result, this, [job, &result] {
0113             qDebug() << job->error() << job->errorString() << job->errorText();
0114             result = (job->error() == KAuth::ActionReply::NoError);
0115         });
0116         job->exec();
0117         return result;
0118     }
0119 
0120     bool isLiveSession()
0121     {
0122         QFile cmdline("/proc/cmdline");
0123         if (!cmdline.open(QFile::ReadOnly)) {
0124             return false;
0125         }
0126         if (cmdline.readAll().contains(QByteArray("boot=casper"))) { // ubuntus
0127             return true;
0128         }
0129         return false;
0130     }
0131 
0132     void setBusy(bool busy)
0133     {
0134         if (m_busy == busy) {
0135             return;
0136         }
0137         m_busy = busy;
0138         Q_EMIT busyChanged();
0139     }
0140 
0141     const bool m_grubCfgExists = grubCfgExists();
0142     bool m_disabled = false;
0143     bool m_busy = false;
0144     QString m_error;
0145     const bool m_liveSession = isLiveSession();
0146     // make this a function should it cover more than neon at some point
0147     const bool m_livePersistent = (KOSRelease().id() == QLatin1String("neon"));
0148 };
0149 
0150 // QML is fairly heavy. Only load it on demand. This class wraps an entire engine's life time allowing us
0151 // to throw it away from the qml side (effectively a glorified window hide).
0152 class LifeTimeWrapper : public QObject
0153 {
0154     Q_OBJECT
0155 public:
0156     using QObject::QObject;
0157     Q_INVOKABLE void quit()
0158     {
0159         deleteLater();
0160     }
0161 };
0162 
0163 int main(int argc, char **argv)
0164 {
0165     QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
0166     QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
0167 
0168     QApplication app(argc, argv);
0169     app.setWindowIcon(QIcon::fromTheme(QStringLiteral("video-card-inactive")));
0170     app.setDesktopFileName(QStringLiteral("org.kde.nomodeset"));
0171 
0172     AuthHelper helper;
0173     if (helper.shouldIgnore()) {
0174         // nomodeset was disabled in this boot already. Don't show the notification anymore until a reboot happens.
0175         qDebug() << "Marker file exists - a change is pending and needs a reboot";
0176         return 0;
0177     }
0178 
0179     KStatusNotifierItem item;
0180     item.setIconByName("video-card-inactive");
0181     item.setToolTipIconByName("video-card-inactive");
0182     item.setTitle(i18nc("@title", "Safe Graphics Mode Warning"));
0183     item.setToolTipTitle(i18nc("@title systray tooltip title", "Safe Graphics Mode"));
0184     item.setToolTipSubTitle(i18nc("@info:tooltip",
0185                                   "The system currently runs in Safe Graphics Mode - graphics card and display performance may be impaired"));
0186     item.setStatus(KStatusNotifierItem::NeedsAttention);
0187     item.setCategory(KStatusNotifierItem::SystemServices);
0188     item.setStandardActionsEnabled(true);
0189 
0190     QObject::connect(&item, &KStatusNotifierItem::activateRequested, [&item, &helper] {
0191         if (!qApp->allWindows().isEmpty()) { // already open
0192             const QWindowList windows = qApp->allWindows();
0193             for (auto window : windows) {
0194                 if (window->isVisible()) {
0195                     window->requestActivate();
0196                 }
0197             }
0198             return;
0199         }
0200         // No longer needs attention but will still be active. We really want the user to fix nomodeset.
0201         item.setStatus(KStatusNotifierItem::Active);
0202 
0203         auto wrapper = new LifeTimeWrapper(&helper);
0204         auto engine = new QQmlApplicationEngine(wrapper);
0205 
0206         auto l10nContext = new KLocalizedContext(engine);
0207         l10nContext->setTranslationDomain(QStringLiteral(TRANSLATION_DOMAIN));
0208 
0209         engine->rootContext()->setContextObject(l10nContext);
0210         engine->rootContext()->setContextProperty("AuthHelper", &helper);
0211         engine->rootContext()->setContextProperty("LifeTimeWrapper", wrapper);
0212 
0213         KDeclarative::KDeclarative::setupEngine(engine);
0214 
0215         engine->load(QUrl(QStringLiteral("qrc:/main.qml")));
0216     });
0217 
0218     return app.exec();
0219 }
0220 
0221 #include "main.moc"