1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2001 Waldo Bastian <bastian@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-only
6*/
7
8#include "kconfig_version.h"
9#include <cstdlib>
10
11#include <QCoreApplication>
12#include <QDate>
13#include <QDebug>
14#include <QDir>
15#include <QFile>
16#include <QProcess>
17#include <QTemporaryFile>
18#include <QTextStream>
19#include <QUrl>
20
21#include <kconfig.h>
22#include <kconfiggroup.h>
23
24#include <QCommandLineOption>
25#include <QCommandLineParser>
26#include <QStandardPaths>
27
28#include "kconf_update_debug.h"
29
30// Convenience wrapper around qCDebug to prefix the output with metadata of
31// the file.
32#define qCDebugFile(CATEGORY) qCDebug(CATEGORY) << m_currentFilename << ':' << m_lineCount << ":'" << m_line << "': "
33
34class KonfUpdate
35{
36public:
37 KonfUpdate(QCommandLineParser *parser);
38 ~KonfUpdate();
39
40 KonfUpdate(const KonfUpdate &) = delete;
41 KonfUpdate &operator=(const KonfUpdate &) = delete;
42
43 QStringList findUpdateFiles(bool dirtyOnly);
44
45 bool updateFile(const QString &filename);
46
47 void gotId(const QString &_id);
48 void gotScript(const QString &_script);
49
50protected:
51 /** kconf_updaterc */
52 KConfig *m_config;
53 QString m_currentFilename;
54 bool m_skip = false;
55 bool m_bTestMode;
56 bool m_bDebugOutput;
57 QString m_id;
58
59 bool m_bUseConfigInfo = false;
60 QStringList m_arguments;
61 QTextStream *m_textStream;
62 QFile *m_file;
63 QString m_line;
64 int m_lineCount;
65};
66
67KonfUpdate::KonfUpdate(QCommandLineParser *parser)
68 : m_textStream(nullptr)
69 , m_file(nullptr)
70 , m_lineCount(-1)
71{
72 bool updateAll = false;
73
74 m_config = new KConfig(QStringLiteral("kconf_updaterc"));
75 KConfigGroup cg(m_config, QString());
76
77 QStringList updateFiles;
78
79 m_bDebugOutput = parser->isSet(QStringLiteral("debug"));
80 if (m_bDebugOutput) {
81 // The only way to enable debug reliably is through a filter rule.
82 // The category itself is const, so we can't just go around changing
83 // its mode. This can however be overridden by the environment, so
84 // we'll want to have a fallback warning if debug is not enabled
85 // after setting the filter.
86 QLoggingCategory::setFilterRules(QLatin1String("%1.debug=true").arg(args: QLatin1String{KCONF_UPDATE_LOG().categoryName()}));
87 qDebug() << "Automatically enabled the debug logging category" << KCONF_UPDATE_LOG().categoryName();
88 if (!KCONF_UPDATE_LOG().isDebugEnabled()) {
89 qWarning(msg: "The debug logging category %s needs to be enabled manually to get debug output", KCONF_UPDATE_LOG().categoryName());
90 }
91 }
92
93 m_bTestMode = parser->isSet(QStringLiteral("testmode"));
94 if (m_bTestMode) {
95 QStandardPaths::setTestModeEnabled(true);
96 }
97
98 if (parser->isSet(QStringLiteral("check"))) {
99 m_bUseConfigInfo = true;
100 const QString file =
101 QStandardPaths::locate(type: QStandardPaths::GenericDataLocation, fileName: QLatin1String{"kconf_update/"} + parser->value(QStringLiteral("check")));
102 if (file.isEmpty()) {
103 qWarning(msg: "File '%s' not found.", parser->value(QStringLiteral("check")).toLocal8Bit().data());
104 qCDebug(KCONF_UPDATE_LOG) << "File" << parser->value(QStringLiteral("check")) << "passed on command line not found";
105 return;
106 }
107 updateFiles.append(t: file);
108 } else if (!parser->positionalArguments().isEmpty()) {
109 updateFiles += parser->positionalArguments();
110 } else if (m_bTestMode) {
111 qWarning(msg: "Test mode enabled, but no files given.");
112 return;
113 } else {
114 if (cg.readEntry(key: "autoUpdateDisabled", defaultValue: false)) {
115 return;
116 }
117 updateFiles = findUpdateFiles(dirtyOnly: true);
118 updateAll = true;
119 }
120
121 for (const QString &file : std::as_const(t&: updateFiles)) {
122 updateFile(filename: file);
123 }
124
125 if (updateAll && !cg.readEntry(key: "updateInfoAdded", defaultValue: false)) {
126 cg.writeEntry(key: "updateInfoAdded", value: true);
127 updateFiles = findUpdateFiles(dirtyOnly: false);
128 }
129}
130
131KonfUpdate::~KonfUpdate()
132{
133 delete m_config;
134 delete m_file;
135 delete m_textStream;
136}
137
138QStringList KonfUpdate::findUpdateFiles(bool dirtyOnly)
139{
140 QStringList result;
141
142 const QStringList dirs = QStandardPaths::locateAll(type: QStandardPaths::GenericDataLocation, QStringLiteral("kconf_update"), options: QStandardPaths::LocateDirectory);
143 for (const QString &d : dirs) {
144 const QDir dir(d);
145
146 const QStringList fileNames = dir.entryList(nameFilters: QStringList(QStringLiteral("*.upd")));
147 for (const QString &fileName : fileNames) {
148 const QString file = dir.filePath(fileName);
149 QFileInfo info(file);
150
151 KConfigGroup cg(m_config, fileName);
152 const qint64 ctime = cg.readEntry(key: "ctime", defaultValue: 0);
153 const qint64 mtime = cg.readEntry(key: "mtime", defaultValue: 0);
154 const QString done = cg.readEntry(key: "done", aDefault: QString());
155 if (!dirtyOnly //
156 || (ctime != 0 && ctime != info.birthTime().toSecsSinceEpoch()) //
157 || mtime != info.lastModified().toSecsSinceEpoch() //
158 || (mtime != 0 && done.isEmpty())) {
159 result.append(t: file);
160 }
161 }
162 }
163 return result;
164}
165
166/**
167 * Syntax:
168 * # Comment
169 * Id=id
170 * ScriptArguments=arguments
171 * Script=scriptfile[,interpreter]
172 **/
173bool KonfUpdate::updateFile(const QString &filename)
174{
175 m_currentFilename = filename;
176 const int i = m_currentFilename.lastIndexOf(c: QLatin1Char{'/'});
177 if (i != -1) {
178 m_currentFilename = m_currentFilename.mid(position: i + 1);
179 }
180 QFile file(filename);
181 if (!file.open(flags: QIODevice::ReadOnly)) {
182 qWarning(msg: "Could not open update-file '%s'.", qUtf8Printable(filename));
183 return false;
184 }
185
186 qCDebug(KCONF_UPDATE_LOG) << "Checking update-file" << filename << "for new updates";
187
188 QTextStream ts(&file);
189 ts.setEncoding(QStringConverter::Encoding::Latin1);
190 m_lineCount = 0;
191 bool foundVersion = false;
192 while (!ts.atEnd()) {
193 m_line = ts.readLine().trimmed();
194 const QLatin1String versionPrefix("Version=");
195 if (m_line.startsWith(s: versionPrefix)) {
196 if (m_line.mid(position: versionPrefix.length()) == QLatin1Char('6')) {
197 foundVersion = true;
198 continue;
199 } else {
200 qWarning(catFunc: KCONF_UPDATE_LOG).noquote() << filename << "defined" << m_line << "but Version=6 was expected";
201 return false;
202 }
203 }
204 ++m_lineCount;
205 if (m_line.isEmpty() || (m_line[0] == QLatin1Char('#'))) {
206 continue;
207 }
208 if (m_line.startsWith(s: QLatin1String("Id="))) {
209 if (!foundVersion) {
210 qCDebug(KCONF_UPDATE_LOG, "Missing 'Version=6', file '%s' will be skipped.", qUtf8Printable(filename));
211 break;
212 }
213 gotId(id: m_line.mid(position: 3));
214 } else if (m_skip) {
215 continue;
216 } else if (m_line.startsWith(s: QLatin1String("Script="))) {
217 gotScript(script: m_line.mid(position: 7));
218 m_arguments.clear();
219 } else if (m_line.startsWith(s: QLatin1String("ScriptArguments="))) {
220 const QString argLine = m_line.mid(position: 16);
221 m_arguments = QProcess::splitCommand(command: argLine);
222 } else {
223 qCDebugFile(KCONF_UPDATE_LOG) << "Parse error";
224 }
225 }
226 // Flush.
227 gotId(id: QString());
228
229 // Remember that this file was updated:
230 if (!m_bTestMode) {
231 QFileInfo info(filename);
232 KConfigGroup cg(m_config, m_currentFilename);
233 if (info.birthTime().isValid()) {
234 cg.writeEntry(key: "ctime", value: info.birthTime().toSecsSinceEpoch());
235 }
236 cg.writeEntry(key: "mtime", value: info.lastModified().toSecsSinceEpoch());
237 cg.sync();
238 }
239
240 return true;
241}
242
243void KonfUpdate::gotId(const QString &_id)
244{
245 // Remember that the last update group has been done:
246 if (!m_id.isEmpty() && !m_skip && !m_bTestMode) {
247 KConfigGroup cg(m_config, m_currentFilename);
248
249 QStringList ids = cg.readEntry(key: "done", aDefault: QStringList());
250 if (!ids.contains(str: m_id)) {
251 ids.append(t: m_id);
252 cg.writeEntry(key: "done", value: ids);
253 cg.sync();
254 }
255 }
256
257 if (_id.isEmpty()) {
258 return;
259 }
260
261 // Check whether this update group needs to be done:
262 KConfigGroup cg(m_config, m_currentFilename);
263 QStringList ids = cg.readEntry(key: "done", aDefault: QStringList());
264 if (ids.contains(str: _id) && !m_bUseConfigInfo) {
265 // qDebug("Id '%s' was already in done-list", _id.toLatin1().constData());
266 m_skip = true;
267 return;
268 }
269 m_skip = false;
270 m_id = _id;
271 if (m_bUseConfigInfo) {
272 qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": Checking update" << _id;
273 } else {
274 qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": Found new update" << _id;
275 }
276}
277
278void KonfUpdate::gotScript(const QString &_script)
279{
280 QString script;
281 QString interpreter;
282 const int i = _script.indexOf(c: QLatin1Char{','});
283 if (i == -1) {
284 script = _script.trimmed();
285 } else {
286 script = _script.left(n: i).trimmed();
287 interpreter = _script.mid(position: i + 1).trimmed();
288 }
289
290 if (script.isEmpty()) {
291 qCDebugFile(KCONF_UPDATE_LOG) << "Script fails to specify filename";
292 m_skip = true;
293 return;
294 }
295
296 QString path = QStandardPaths::locate(type: QStandardPaths::GenericDataLocation, fileName: QLatin1String("kconf_update/") + script);
297 if (path.isEmpty()) {
298 if (interpreter.isEmpty()) {
299 path = QStringLiteral("%1/kconf_update_bin/%2").arg(QStringLiteral(CMAKE_INSTALL_FULL_LIBDIR), args&: script);
300 if (!QFile::exists(fileName: path)) {
301 path = QStandardPaths::findExecutable(executableName: script);
302 }
303 }
304
305 if (path.isEmpty()) {
306 qCDebugFile(KCONF_UPDATE_LOG) << "Script" << script << "not found";
307 m_skip = true;
308 return;
309 }
310 }
311
312 if (!m_arguments.isEmpty()) {
313 qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": Running script" << script << "with arguments" << m_arguments;
314 } else {
315 qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": Running script" << script;
316 }
317
318 QStringList args;
319 QString cmd;
320 if (interpreter.isEmpty()) {
321 cmd = path;
322 } else {
323 QString interpreterPath = QStandardPaths::findExecutable(executableName: interpreter);
324 if (interpreterPath.isEmpty()) {
325 qCDebugFile(KCONF_UPDATE_LOG) << "Cannot find interpreter" << interpreter;
326 m_skip = true;
327 return;
328 }
329 cmd = interpreterPath;
330 args << path;
331 }
332
333 args += m_arguments;
334
335 int result;
336 qCDebug(KCONF_UPDATE_LOG) << "About to run" << cmd;
337 if (m_bDebugOutput) {
338 QFile scriptFile(path);
339 if (scriptFile.open(flags: QIODevice::ReadOnly)) {
340 qCDebug(KCONF_UPDATE_LOG) << "Script contents is:\n" << scriptFile.readAll();
341 }
342 }
343 QProcess proc;
344 proc.start(program: cmd, arguments: args);
345 if (!proc.waitForFinished(msecs: 60000)) {
346 qCDebugFile(KCONF_UPDATE_LOG) << "update script did not terminate within 60 seconds:" << cmd;
347 m_skip = true;
348 return;
349 }
350 result = proc.exitCode();
351 proc.close();
352
353 if (result != EXIT_SUCCESS) {
354 qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": !! An error occurred while running" << cmd;
355 return;
356 }
357
358 qCDebug(KCONF_UPDATE_LOG) << "Successfully ran" << cmd;
359}
360
361int main(int argc, char **argv)
362{
363 QCoreApplication app(argc, argv);
364 app.setApplicationVersion(QStringLiteral(KCONFIG_VERSION_STRING));
365
366 QCommandLineParser parser;
367 parser.addVersionOption();
368 parser.setApplicationDescription(QCoreApplication::translate(context: "main", key: "KDE Tool for updating user configuration files"));
369 parser.addHelpOption();
370 parser.addOption(commandLineOption: QCommandLineOption(QStringList{QStringLiteral("debug")}, QCoreApplication::translate(context: "main", key: "Keep output results from scripts")));
371 parser.addOption(commandLineOption: QCommandLineOption(
372 QStringList{QStringLiteral("testmode")},
373 QCoreApplication::translate(context: "main", key: "For unit tests only: do not write the done entries, so that with every re-run, the scripts are executed again")));
374 parser.addOption(commandLineOption: QCommandLineOption(QStringList{QStringLiteral("check")},
375 QCoreApplication::translate(context: "main", key: "Check whether config file itself requires updating"),
376 QStringLiteral("update-file")));
377 parser.addPositionalArgument(QStringLiteral("files"),
378 description: QCoreApplication::translate(context: "main", key: "File(s) to read update instructions from"),
379 QStringLiteral("[files...]"));
380
381 // TODO aboutData.addAuthor(ki18n("Waldo Bastian"), KLocalizedString(), "bastian@kde.org");
382
383 parser.process(app);
384 KonfUpdate konfUpdate(&parser);
385
386 return 0;
387}
388

source code of kconfig/src/kconf_update/kconf_update.cpp