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 }