File indexing completed on 2024-05-12 03:54:29

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2001 Waldo Bastian <bastian@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-only
0006 */
0007 
0008 #include "kconfig_version.h"
0009 #include <cstdlib>
0010 
0011 #include <QCoreApplication>
0012 #include <QDate>
0013 #include <QDebug>
0014 #include <QDir>
0015 #include <QFile>
0016 #include <QProcess>
0017 #include <QTemporaryFile>
0018 #include <QTextStream>
0019 #include <QUrl>
0020 
0021 #include <kconfig.h>
0022 #include <kconfiggroup.h>
0023 
0024 #include <QCommandLineOption>
0025 #include <QCommandLineParser>
0026 #include <QStandardPaths>
0027 
0028 #include "kconf_update_debug.h"
0029 
0030 // Convenience wrapper around qCDebug to prefix the output with metadata of
0031 // the file.
0032 #define qCDebugFile(CATEGORY) qCDebug(CATEGORY) << m_currentFilename << ':' << m_lineCount << ":'" << m_line << "': "
0033 
0034 class KonfUpdate
0035 {
0036 public:
0037     KonfUpdate(QCommandLineParser *parser);
0038     ~KonfUpdate();
0039 
0040     KonfUpdate(const KonfUpdate &) = delete;
0041     KonfUpdate &operator=(const KonfUpdate &) = delete;
0042 
0043     QStringList findUpdateFiles(bool dirtyOnly);
0044 
0045     bool updateFile(const QString &filename);
0046 
0047     void gotId(const QString &_id);
0048     void gotScript(const QString &_script);
0049 
0050 protected:
0051     /** kconf_updaterc */
0052     KConfig *m_config;
0053     QString m_currentFilename;
0054     bool m_skip = false;
0055     bool m_bTestMode;
0056     bool m_bDebugOutput;
0057     QString m_id;
0058 
0059     bool m_bUseConfigInfo = false;
0060     QStringList m_arguments;
0061     QTextStream *m_textStream;
0062     QFile *m_file;
0063     QString m_line;
0064     int m_lineCount;
0065 };
0066 
0067 KonfUpdate::KonfUpdate(QCommandLineParser *parser)
0068     : m_textStream(nullptr)
0069     , m_file(nullptr)
0070     , m_lineCount(-1)
0071 {
0072     bool updateAll = false;
0073 
0074     m_config = new KConfig(QStringLiteral("kconf_updaterc"));
0075     KConfigGroup cg(m_config, QString());
0076 
0077     QStringList updateFiles;
0078 
0079     m_bDebugOutput = parser->isSet(QStringLiteral("debug"));
0080     if (m_bDebugOutput) {
0081         // The only way to enable debug reliably is through a filter rule.
0082         // The category itself is const, so we can't just go around changing
0083         // its mode. This can however be overridden by the environment, so
0084         // we'll want to have a fallback warning if debug is not enabled
0085         // after setting the filter.
0086         QLoggingCategory::setFilterRules(QLatin1String("%1.debug=true").arg(QLatin1String{KCONF_UPDATE_LOG().categoryName()}));
0087         qDebug() << "Automatically enabled the debug logging category" << KCONF_UPDATE_LOG().categoryName();
0088         if (!KCONF_UPDATE_LOG().isDebugEnabled()) {
0089             qWarning("The debug logging category %s needs to be enabled manually to get debug output", KCONF_UPDATE_LOG().categoryName());
0090         }
0091     }
0092 
0093     m_bTestMode = parser->isSet(QStringLiteral("testmode"));
0094     if (m_bTestMode) {
0095         QStandardPaths::setTestModeEnabled(true);
0096     }
0097 
0098     if (parser->isSet(QStringLiteral("check"))) {
0099         m_bUseConfigInfo = true;
0100         const QString file =
0101             QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String{"kconf_update/"} + parser->value(QStringLiteral("check")));
0102         if (file.isEmpty()) {
0103             qWarning("File '%s' not found.", parser->value(QStringLiteral("check")).toLocal8Bit().data());
0104             qCDebug(KCONF_UPDATE_LOG) << "File" << parser->value(QStringLiteral("check")) << "passed on command line not found";
0105             return;
0106         }
0107         updateFiles.append(file);
0108     } else if (!parser->positionalArguments().isEmpty()) {
0109         updateFiles += parser->positionalArguments();
0110     } else if (m_bTestMode) {
0111         qWarning("Test mode enabled, but no files given.");
0112         return;
0113     } else {
0114         if (cg.readEntry("autoUpdateDisabled", false)) {
0115             return;
0116         }
0117         updateFiles = findUpdateFiles(true);
0118         updateAll = true;
0119     }
0120 
0121     for (const QString &file : std::as_const(updateFiles)) {
0122         updateFile(file);
0123     }
0124 
0125     if (updateAll && !cg.readEntry("updateInfoAdded", false)) {
0126         cg.writeEntry("updateInfoAdded", true);
0127         updateFiles = findUpdateFiles(false);
0128     }
0129 }
0130 
0131 KonfUpdate::~KonfUpdate()
0132 {
0133     delete m_config;
0134     delete m_file;
0135     delete m_textStream;
0136 }
0137 
0138 QStringList KonfUpdate::findUpdateFiles(bool dirtyOnly)
0139 {
0140     QStringList result;
0141 
0142     const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kconf_update"), QStandardPaths::LocateDirectory);
0143     for (const QString &d : dirs) {
0144         const QDir dir(d);
0145 
0146         const QStringList fileNames = dir.entryList(QStringList(QStringLiteral("*.upd")));
0147         for (const QString &fileName : fileNames) {
0148             const QString file = dir.filePath(fileName);
0149             QFileInfo info(file);
0150 
0151             KConfigGroup cg(m_config, fileName);
0152             const qint64 ctime = cg.readEntry("ctime", 0);
0153             const qint64 mtime = cg.readEntry("mtime", 0);
0154             if (!dirtyOnly //
0155                 || (ctime != 0 && ctime != info.birthTime().toSecsSinceEpoch()) //
0156                 || mtime != info.lastModified().toSecsSinceEpoch()) {
0157                 result.append(file);
0158             }
0159         }
0160     }
0161     return result;
0162 }
0163 
0164 /**
0165  * Syntax:
0166  * # Comment
0167  * Id=id
0168  * ScriptArguments=arguments
0169  * Script=scriptfile[,interpreter]
0170  **/
0171 bool KonfUpdate::updateFile(const QString &filename)
0172 {
0173     m_currentFilename = filename;
0174     const int i = m_currentFilename.lastIndexOf(QLatin1Char{'/'});
0175     if (i != -1) {
0176         m_currentFilename = m_currentFilename.mid(i + 1);
0177     }
0178     QFile file(filename);
0179     if (!file.open(QIODevice::ReadOnly)) {
0180         qWarning("Could not open update-file '%s'.", qUtf8Printable(filename));
0181         return false;
0182     }
0183 
0184     qCDebug(KCONF_UPDATE_LOG) << "Checking update-file" << filename << "for new updates";
0185 
0186     QTextStream ts(&file);
0187     ts.setEncoding(QStringConverter::Encoding::Latin1);
0188     m_lineCount = 0;
0189     bool foundVersion = false;
0190     while (!ts.atEnd()) {
0191         m_line = ts.readLine().trimmed();
0192         const QLatin1String versionPrefix("Version=");
0193         if (m_line.startsWith(versionPrefix)) {
0194             if (m_line.mid(versionPrefix.length()) == QLatin1Char('6')) {
0195                 foundVersion = true;
0196                 continue;
0197             } else {
0198                 qWarning(KCONF_UPDATE_LOG).noquote() << filename << "defined" << m_line << "but Version=6 was expected";
0199                 return false;
0200             }
0201         }
0202         ++m_lineCount;
0203         if (m_line.isEmpty() || (m_line[0] == QLatin1Char('#'))) {
0204             continue;
0205         }
0206         if (m_line.startsWith(QLatin1String("Id="))) {
0207             if (!foundVersion) {
0208                 qCDebug(KCONF_UPDATE_LOG, "Missing 'Version=6', file '%s' will be skipped.", qUtf8Printable(filename));
0209                 break;
0210             }
0211             gotId(m_line.mid(3));
0212         } else if (m_skip) {
0213             continue;
0214         } else if (m_line.startsWith(QLatin1String("Script="))) {
0215             gotScript(m_line.mid(7));
0216             m_arguments.clear();
0217         } else if (m_line.startsWith(QLatin1String("ScriptArguments="))) {
0218             const QString argLine = m_line.mid(16);
0219             m_arguments = QProcess::splitCommand(argLine);
0220         } else {
0221             qCDebugFile(KCONF_UPDATE_LOG) << "Parse error";
0222         }
0223     }
0224     // Flush.
0225     gotId(QString());
0226 
0227     // Remember that this file was updated:
0228     if (!m_bTestMode) {
0229         QFileInfo info(filename);
0230         KConfigGroup cg(m_config, m_currentFilename);
0231         if (info.birthTime().isValid()) {
0232             cg.writeEntry("ctime", info.birthTime().toSecsSinceEpoch());
0233         }
0234         cg.writeEntry("mtime", info.lastModified().toSecsSinceEpoch());
0235         cg.sync();
0236     }
0237 
0238     return true;
0239 }
0240 
0241 void KonfUpdate::gotId(const QString &_id)
0242 {
0243     // Remember that the last update group has been done:
0244     if (!m_id.isEmpty() && !m_skip && !m_bTestMode) {
0245         KConfigGroup cg(m_config, m_currentFilename);
0246 
0247         QStringList ids = cg.readEntry("done", QStringList());
0248         if (!ids.contains(m_id)) {
0249             ids.append(m_id);
0250             cg.writeEntry("done", ids);
0251             cg.sync();
0252         }
0253     }
0254 
0255     if (_id.isEmpty()) {
0256         return;
0257     }
0258 
0259     // Check whether this update group needs to be done:
0260     KConfigGroup cg(m_config, m_currentFilename);
0261     QStringList ids = cg.readEntry("done", QStringList());
0262     if (ids.contains(_id) && !m_bUseConfigInfo) {
0263         // qDebug("Id '%s' was already in done-list", _id.toLatin1().constData());
0264         m_skip = true;
0265         return;
0266     }
0267     m_skip = false;
0268     m_id = _id;
0269     if (m_bUseConfigInfo) {
0270         qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": Checking update" << _id;
0271     } else {
0272         qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": Found new update" << _id;
0273     }
0274 }
0275 
0276 void KonfUpdate::gotScript(const QString &_script)
0277 {
0278     QString script;
0279     QString interpreter;
0280     const int i = _script.indexOf(QLatin1Char{','});
0281     if (i == -1) {
0282         script = _script.trimmed();
0283     } else {
0284         script = _script.left(i).trimmed();
0285         interpreter = _script.mid(i + 1).trimmed();
0286     }
0287 
0288     if (script.isEmpty()) {
0289         qCDebugFile(KCONF_UPDATE_LOG) << "Script fails to specify filename";
0290         m_skip = true;
0291         return;
0292     }
0293 
0294     QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kconf_update/") + script);
0295     if (path.isEmpty()) {
0296         if (interpreter.isEmpty()) {
0297             path = QStringLiteral("%1/kconf_update_bin/%2").arg(QStringLiteral(CMAKE_INSTALL_FULL_LIBDIR), script);
0298             if (!QFile::exists(path)) {
0299                 path = QStandardPaths::findExecutable(script);
0300             }
0301         }
0302 
0303         if (path.isEmpty()) {
0304             qCDebugFile(KCONF_UPDATE_LOG) << "Script" << script << "not found";
0305             m_skip = true;
0306             return;
0307         }
0308     }
0309 
0310     if (!m_arguments.isEmpty()) {
0311         qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": Running script" << script << "with arguments" << m_arguments;
0312     } else {
0313         qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": Running script" << script;
0314     }
0315 
0316     QStringList args;
0317     QString cmd;
0318     if (interpreter.isEmpty()) {
0319         cmd = path;
0320     } else {
0321         QString interpreterPath = QStandardPaths::findExecutable(interpreter);
0322         if (interpreterPath.isEmpty()) {
0323             qCDebugFile(KCONF_UPDATE_LOG) << "Cannot find interpreter" << interpreter;
0324             m_skip = true;
0325             return;
0326         }
0327         cmd = interpreterPath;
0328         args << path;
0329     }
0330 
0331     args += m_arguments;
0332 
0333     int result;
0334     qCDebug(KCONF_UPDATE_LOG) << "About to run" << cmd;
0335     if (m_bDebugOutput) {
0336         QFile scriptFile(path);
0337         if (scriptFile.open(QIODevice::ReadOnly)) {
0338             qCDebug(KCONF_UPDATE_LOG) << "Script contents is:\n" << scriptFile.readAll();
0339         }
0340     }
0341     QProcess proc;
0342     proc.start(cmd, args);
0343     if (!proc.waitForFinished(60000)) {
0344         qCDebugFile(KCONF_UPDATE_LOG) << "update script did not terminate within 60 seconds:" << cmd;
0345         m_skip = true;
0346         return;
0347     }
0348     result = proc.exitCode();
0349     proc.close();
0350 
0351     if (result != EXIT_SUCCESS) {
0352         qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": !! An error occurred while running" << cmd;
0353         return;
0354     }
0355 
0356     qCDebug(KCONF_UPDATE_LOG) << "Successfully ran" << cmd;
0357 }
0358 
0359 int main(int argc, char **argv)
0360 {
0361     QCoreApplication app(argc, argv);
0362     app.setApplicationVersion(QStringLiteral(KCONFIG_VERSION_STRING));
0363 
0364     QCommandLineParser parser;
0365     parser.addVersionOption();
0366     parser.setApplicationDescription(QCoreApplication::translate("main", "KDE Tool for updating user configuration files"));
0367     parser.addHelpOption();
0368     parser.addOption(QCommandLineOption(QStringList{QStringLiteral("debug")}, QCoreApplication::translate("main", "Keep output results from scripts")));
0369     parser.addOption(QCommandLineOption(
0370         QStringList{QStringLiteral("testmode")},
0371         QCoreApplication::translate("main", "For unit tests only: do not write the done entries, so that with every re-run, the scripts are executed again")));
0372     parser.addOption(QCommandLineOption(QStringList{QStringLiteral("check")},
0373                                         QCoreApplication::translate("main", "Check whether config file itself requires updating"),
0374                                         QStringLiteral("update-file")));
0375     parser.addPositionalArgument(QStringLiteral("files"),
0376                                  QCoreApplication::translate("main", "File(s) to read update instructions from"),
0377                                  QStringLiteral("[files...]"));
0378 
0379     // TODO aboutData.addAuthor(ki18n("Waldo Bastian"), KLocalizedString(), "bastian@kde.org");
0380 
0381     parser.process(app);
0382     KonfUpdate konfUpdate(&parser);
0383 
0384     return 0;
0385 }