KConfig

kconf_update.cpp
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 
34 class KonfUpdate
35 {
36 public:
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 
50 protected:
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 
67 KonfUpdate::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(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("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) {
96  }
97 
98  if (parser->isSet(QStringLiteral("check"))) {
99  m_bUseConfigInfo = true;
100  const QString file =
101  QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String{"kconf_update/"} + parser->value(QStringLiteral("check")));
102  if (file.isEmpty()) {
103  qWarning("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(file);
108  } else if (!parser->positionalArguments().isEmpty()) {
109  updateFiles += parser->positionalArguments();
110  } else if (m_bTestMode) {
111  qWarning("Test mode enabled, but no files given.");
112  return;
113  } else {
114  if (cg.readEntry("autoUpdateDisabled", false)) {
115  return;
116  }
117  updateFiles = findUpdateFiles(true);
118  updateAll = true;
119  }
120 
121  for (const QString &file : std::as_const(updateFiles)) {
122  updateFile(file);
123  }
124 
125  if (updateAll && !cg.readEntry("updateInfoAdded", false)) {
126  cg.writeEntry("updateInfoAdded", true);
127  updateFiles = findUpdateFiles(false);
128  }
129 }
130 
131 KonfUpdate::~KonfUpdate()
132 {
133  delete m_config;
134  delete m_file;
135  delete m_textStream;
136 }
137 
138 QStringList KonfUpdate::findUpdateFiles(bool dirtyOnly)
139 {
140  QStringList result;
141 
143  for (const QString &d : dirs) {
144  const QDir dir(d);
145 
146  const QStringList fileNames = dir.entryList(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("ctime", 0);
153  const qint64 mtime = cg.readEntry("mtime", 0);
154  if (!dirtyOnly //
155  || (ctime != 0 && ctime != info.birthTime().toSecsSinceEpoch()) //
156  || mtime != info.lastModified().toSecsSinceEpoch()) {
157  result.append(file);
158  }
159  }
160  }
161  return result;
162 }
163 
164 /**
165  * Syntax:
166  * # Comment
167  * Id=id
168  * ScriptArguments=arguments
169  * Script=scriptfile[,interpreter]
170  **/
171 bool KonfUpdate::updateFile(const QString &filename)
172 {
173  m_currentFilename = filename;
174  const int i = m_currentFilename.lastIndexOf(QLatin1Char{'/'});
175  if (i != -1) {
176  m_currentFilename = m_currentFilename.mid(i + 1);
177  }
178  QFile file(filename);
179  if (!file.open(QIODevice::ReadOnly)) {
180  qWarning("Could not open update-file '%s'.", qUtf8Printable(filename));
181  return false;
182  }
183 
184  qCDebug(KCONF_UPDATE_LOG) << "Checking update-file" << filename << "for new updates";
185 
186  QTextStream ts(&file);
187  ts.setEncoding(QStringConverter::Encoding::Latin1);
188  m_lineCount = 0;
189  bool foundVersion = false;
190  while (!ts.atEnd()) {
191  m_line = ts.readLine().trimmed();
192  const QLatin1String versionPrefix("Version=");
193  if (m_line.startsWith(versionPrefix)) {
194  if (m_line.mid(versionPrefix.length()) == QLatin1Char('6')) {
195  foundVersion = true;
196  continue;
197  } else {
198  qWarning(KCONF_UPDATE_LOG).noquote() << filename << "defined" << m_line << "but Version=6 was expected";
199  return false;
200  }
201  }
202  ++m_lineCount;
203  if (m_line.isEmpty() || (m_line[0] == QLatin1Char('#'))) {
204  continue;
205  }
206  if (m_line.startsWith(QLatin1String("Id="))) {
207  if (!foundVersion) {
208  qCDebug(KCONF_UPDATE_LOG, "Missing 'Version=6', file '%s' will be skipped.", qUtf8Printable(filename));
209  break;
210  }
211  gotId(m_line.mid(3));
212  } else if (m_skip) {
213  continue;
214  } else if (m_line.startsWith(QLatin1String("Script="))) {
215  gotScript(m_line.mid(7));
216  m_arguments.clear();
217  } else if (m_line.startsWith(QLatin1String("ScriptArguments="))) {
218  const QString argLine = m_line.mid(16);
219  m_arguments = QProcess::splitCommand(argLine);
220  } else {
221  qCDebugFile(KCONF_UPDATE_LOG) << "Parse error";
222  }
223  }
224  // Flush.
225  gotId(QString());
226 
227  // Remember that this file was updated:
228  if (!m_bTestMode) {
229  QFileInfo info(filename);
230  KConfigGroup cg(m_config, m_currentFilename);
231  if (info.birthTime().isValid()) {
232  cg.writeEntry("ctime", info.birthTime().toSecsSinceEpoch());
233  }
234  cg.writeEntry("mtime", info.lastModified().toSecsSinceEpoch());
235  cg.sync();
236  }
237 
238  return true;
239 }
240 
241 void KonfUpdate::gotId(const QString &_id)
242 {
243  // Remember that the last update group has been done:
244  if (!m_id.isEmpty() && !m_skip && !m_bTestMode) {
245  KConfigGroup cg(m_config, m_currentFilename);
246 
247  QStringList ids = cg.readEntry("done", QStringList());
248  if (!ids.contains(m_id)) {
249  ids.append(m_id);
250  cg.writeEntry("done", ids);
251  cg.sync();
252  }
253  }
254 
255  if (_id.isEmpty()) {
256  return;
257  }
258 
259  // Check whether this update group needs to be done:
260  KConfigGroup cg(m_config, m_currentFilename);
261  QStringList ids = cg.readEntry("done", QStringList());
262  if (ids.contains(_id) && !m_bUseConfigInfo) {
263  // qDebug("Id '%s' was already in done-list", _id.toLatin1().constData());
264  m_skip = true;
265  return;
266  }
267  m_skip = false;
268  m_id = _id;
269  if (m_bUseConfigInfo) {
270  qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": Checking update" << _id;
271  } else {
272  qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": Found new update" << _id;
273  }
274 }
275 
276 void KonfUpdate::gotScript(const QString &_script)
277 {
278  QString script;
279  QString interpreter;
280  const int i = _script.indexOf(QLatin1Char{','});
281  if (i == -1) {
282  script = _script.trimmed();
283  } else {
284  script = _script.left(i).trimmed();
285  interpreter = _script.mid(i + 1).trimmed();
286  }
287 
288  if (script.isEmpty()) {
289  qCDebugFile(KCONF_UPDATE_LOG) << "Script fails to specify filename";
290  m_skip = true;
291  return;
292  }
293 
295  if (path.isEmpty()) {
296  if (interpreter.isEmpty()) {
297  path = QStringLiteral("%1/kconf_update_bin/%2").arg(QStringLiteral(CMAKE_INSTALL_FULL_LIBDIR), script);
298  if (!QFile::exists(path)) {
300  }
301  }
302 
303  if (path.isEmpty()) {
304  qCDebugFile(KCONF_UPDATE_LOG) << "Script" << script << "not found";
305  m_skip = true;
306  return;
307  }
308  }
309 
310  if (!m_arguments.isEmpty()) {
311  qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": Running script" << script << "with arguments" << m_arguments;
312  } else {
313  qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": Running script" << script;
314  }
315 
316  QStringList args;
317  QString cmd;
318  if (interpreter.isEmpty()) {
319  cmd = path;
320  } else {
321  QString interpreterPath = QStandardPaths::findExecutable(interpreter);
322  if (interpreterPath.isEmpty()) {
323  qCDebugFile(KCONF_UPDATE_LOG) << "Cannot find interpreter" << interpreter;
324  m_skip = true;
325  return;
326  }
327  cmd = interpreterPath;
328  args << path;
329  }
330 
331  args += m_arguments;
332 
333  int result;
334  qCDebug(KCONF_UPDATE_LOG) << "About to run" << cmd;
335  if (m_bDebugOutput) {
336  QFile scriptFile(path);
337  if (scriptFile.open(QIODevice::ReadOnly)) {
338  qCDebug(KCONF_UPDATE_LOG) << "Script contents is:\n" << scriptFile.readAll();
339  }
340  }
341  QProcess proc;
342  proc.start(cmd, args);
343  if (!proc.waitForFinished(60000)) {
344  qCDebugFile(KCONF_UPDATE_LOG) << "update script did not terminate within 60 seconds:" << cmd;
345  m_skip = true;
346  return;
347  }
348  result = proc.exitCode();
349  proc.close();
350 
351  if (result != EXIT_SUCCESS) {
352  qCDebug(KCONF_UPDATE_LOG) << m_currentFilename << ": !! An error occurred while running" << cmd;
353  return;
354  }
355 
356  qCDebug(KCONF_UPDATE_LOG) << "Successfully ran" << cmd;
357 }
358 
359 int main(int argc, char **argv)
360 {
361  QCoreApplication app(argc, argv);
362  app.setApplicationVersion(QStringLiteral(KCONFIG_VERSION_STRING));
363 
364  QCommandLineParser parser;
365  parser.addVersionOption();
366  parser.setApplicationDescription(QCoreApplication::translate("main", "KDE Tool for updating user configuration files"));
367  parser.addHelpOption();
368  parser.addOption(QCommandLineOption(QStringList{QStringLiteral("debug")}, QCoreApplication::translate("main", "Keep output results from scripts")));
370  QStringList{QStringLiteral("testmode")},
371  QCoreApplication::translate("main", "For unit tests only: do not write the done entries, so that with every re-run, the scripts are executed again")));
372  parser.addOption(QCommandLineOption(QStringList{QStringLiteral("check")},
373  QCoreApplication::translate("main", "Check whether config file itself requires updating"),
374  QStringLiteral("update-file")));
375  parser.addPositionalArgument(QStringLiteral("files"),
376  QCoreApplication::translate("main", "File(s) to read update instructions from"),
377  QStringLiteral("[files...]"));
378 
379  // TODO aboutData.addAuthor(ki18n("Waldo Bastian"), KLocalizedString(), "bastian@kde.org");
380 
381  parser.process(app);
382  KonfUpdate konfUpdate(&parser);
383 
384  return 0;
385 }
void start(const QString &program, const QStringList &arguments, QIODevice::OpenMode mode)
void append(const T &value)
bool waitForFinished(int msecs)
void setApplicationDescription(const QString &description)
QString translate(const char *context, const char *sourceText, const char *disambiguation, int n)
bool contains(const QString &str, Qt::CaseSensitivity cs) const const
QString trimmed() const const
The central class of the KDE configuration data system.
Definition: kconfig.h:55
void addPositionalArgument(const QString &name, const QString &description, const QString &syntax)
QStringList positionalArguments() const const
QStringList splitCommand(QStringView command)
int lastIndexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
QString locate(QStandardPaths::StandardLocation type, const QString &fileName, QStandardPaths::LocateOptions options)
bool exists() const const
KEDUVOCDOCUMENT_EXPORT QStringList fileNames(const QString &language=QString())
QString findExecutable(const QString &executableName, const QStringList &paths)
void process(const QStringList &arguments)
QCommandLineOption addVersionOption()
bool isEmpty() const const
QString value(const QString &optionName) const const
virtual void close() override
bool isEmpty() const const
int indexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
bool isSet(const QString &name) const const
KIOCORE_EXPORT QString dir(const QString &fileClass)
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
QString path(const QString &relativePath)
QString left(int n) const const
QStringList locateAll(QStandardPaths::StandardLocation type, const QString &fileName, QStandardPaths::LocateOptions options)
bool addOption(const QCommandLineOption &option)
void setTestModeEnabled(bool testMode)
QString mid(int position, int n) const const
QCommandLineOption addHelpOption()
void setFilterRules(const QString &rules)
int exitCode() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Thu Feb 15 2024 04:07:59 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.