KNewStuff

transaction.cpp
1/*
2 SPDX-FileCopyrightText: 2023 Aleix Pol Gonzalez <aleixpol@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.1-or-later
5*/
6
7#include "transaction.h"
8#include "enginebase.h"
9#include "enginebase_p.h"
10#include "entry_p.h"
11#include "providerbase_p.h"
12#include "providercore.h"
13#include "providercore_p.h"
14#include "question.h"
15
16#include <KLocalizedString>
17#include <KShell>
18#include <QDir>
19#include <QProcess>
20#include <QTimer>
21#include <QVersionNumber>
22
23#include <knewstuffcore_debug.h>
24
25using namespace KNSCore;
26
27namespace
28{
29std::optional<int> linkIdFromVersions(const QList<DownloadLinkInformationV2Private> &downloadLinksInformationList)
30{
31 switch (downloadLinksInformationList.size()) {
32 case 0:
33 return {};
34 case 1:
35 return downloadLinksInformationList.at(0).id;
36 }
37
38 QMap<QVersionNumber, int> infoByVersion;
39 for (const auto &info : downloadLinksInformationList) {
40 const auto number = QVersionNumber::fromString(info.version);
41 if (number.isNull()) {
42 qCDebug(KNEWSTUFFCORE) << "Found no valid version number on linkid" << info.id << info.version;
43 continue;
44 }
45 if (infoByVersion.contains(number)) {
46 qCWarning(KNEWSTUFFCORE) << "Encountered version number" << info.version << "more than once. Ignoring duplicates." << info.distributionType;
47 continue;
48 }
49 infoByVersion[number] = info.id;
50 }
51
52 if (infoByVersion.isEmpty()) { // found no valid version
53 return {};
54 }
55
56 return infoByVersion.last(); // map is sorted by keys, highest version is last entry.
57}
58} // namespace
59
60class KNSCore::TransactionPrivate
61{
62public:
63 [[nodiscard]] static Transaction *createInstallTransaction(const KNSCore::Entry &_entry, EngineBase *engine, int linkId)
64 {
65 auto ret = new Transaction(_entry, engine);
66 QObject::connect(engine->d->installation, &Installation::signalInstallationError, ret, [ret, _entry](const QString &msg, const KNSCore::Entry &entry) {
67 if (_entry.uniqueId() == entry.uniqueId()) {
68 Q_EMIT ret->signalErrorCode(KNSCore::ErrorCode::InstallationError, msg, {});
69 }
70 });
71 QTimer::singleShot(0, ret, [ret, linkId] {
72 ret->d->installLinkId(linkId);
73 });
74 return ret;
75 }
76
77 TransactionPrivate(const KNSCore::Entry &entry, EngineBase *engine, Transaction *q)
78 : m_engine(engine)
79 , q(q)
80 , subject(entry)
81 {
82 }
83
84 void finish()
85 {
86 m_finished = true;
87 Q_EMIT q->finished();
88 q->deleteLater();
89 }
90
91 int findLinkIdToInstall(KNSCore::Entry &entry)
92 {
93 const auto downloadLinksInformationList = entry.d.constData()->mDownloadLinkInformationList;
94 const auto optionalLinkId = linkIdFromVersions(downloadLinksInformationList);
95 if (optionalLinkId.has_value()) {
96 qCDebug(KNEWSTUFFCORE) << "Found linkid by version" << optionalLinkId.value();
97 payloadToIdentify[entry] = QString{};
98 return optionalLinkId.value();
99 }
100
101 if (downloadLinksInformationList.size() == 1 || !entry.payload().isEmpty()) {
102 // If there is only one downloadable item (which also includes a predefined payload name), then we can fairly safely assume that's
103 // what we're wanting to update, meaning we can bypass some of the more expensive operations in downloadLinkLoaded
104 qCDebug(KNEWSTUFFCORE) << "Just the one download link, so let's use that";
105 payloadToIdentify[entry] = QString{};
106 return 1;
107 }
108
109 qCDebug(KNEWSTUFFCORE) << "Try and identify a download link to use from a total of" << entry.downloadLinkCount();
110 // While this seems silly, the payload gets reset when fetching the new download link information
111 payloadToIdentify[entry] = entry.payload();
112 // Drop a fresh list in place so we've got something to work with when we get the links
113 payloads[entry] = QStringList{};
114 return 1;
115 }
116
117 // linkid may be -1 to denote the latest link id
118 void installLinkId(int linkId)
119 {
120 if (subject.downloadLinkCount() == 0 && subject.payload().isEmpty()) {
121 // Turns out this happens sometimes, so we should deal with that and spit out an error
122 qCDebug(KNEWSTUFFCORE) << "There were no downloadlinks defined in the entry we were just asked to update: " << subject.uniqueId() << "on provider"
123 << subject.providerId();
124 Q_EMIT q->signalErrorCode(
125 KNSCore::ErrorCode::InstallationError,
126 i18n("Could not perform an installation of the entry %1 as it does not have any downloadable items defined. Please contact the "
127 "author so they can fix this.",
128 subject.name()),
129 subject.uniqueId());
130 finish();
131 return;
132 }
133
134 KNSCore::Entry entry = subject;
135 if (entry.status() == KNSCore::Entry::Updateable) {
136 entry.setStatus(KNSCore::Entry::Updating);
137 } else {
138 entry.setStatus(KNSCore::Entry::Installing);
139 }
140 Q_EMIT q->signalEntryEvent(entry, Entry::StatusChangedEvent);
141
142 qCDebug(KNEWSTUFFCORE) << "Install " << entry.name() << " from: " << entry.providerId();
143 auto provider = m_engine->d->providerCores.value(entry.providerId());
144 if (!provider) {
145 return;
146 }
147
148 QObject::connect(provider->d->base, &ProviderBase::payloadLinkLoaded, q, &Transaction::downloadLinkLoaded);
149 // If linkId is -1, assume we don't know what to update
150 if (linkId == -1) {
151 linkId = findLinkIdToInstall(entry);
152 } else {
153 qCDebug(KNEWSTUFFCORE) << "Link ID already known" << linkId;
154 // If there is no payload to identify, we will assume the payload is already known and just use that
155 payloadToIdentify[entry] = QString{};
156 }
157
158 provider->d->base->loadPayloadLink(entry, linkId);
159
160 m_finished = false;
161 m_engine->updateStatus();
162 }
163
164 EngineBase *const m_engine;
165 Transaction *const q;
166 bool m_finished = false;
167 // Used for updating purposes - we ought to be saving this information, but we also have to deal with old stuff, and so... this will have to do for now
168 // TODO KF6: Installed state needs to move onto a per-downloadlink basis rather than per-entry
170 QMap<Entry, QString> payloadToIdentify;
171 const Entry subject;
172};
173
174/**
175 * we look for the directory where all the resources got installed.
176 * assuming it was extracted into a directory
177 */
178static QDir sharedDir(QStringList dirs, QString rootPath)
179{
180 // Ensure that rootPath definitely is a clean path with a slash at the end
181 rootPath = QDir::cleanPath(rootPath) + QStringLiteral("/");
182 qCInfo(KNEWSTUFFCORE) << Q_FUNC_INFO << dirs << rootPath;
183 while (!dirs.isEmpty()) {
184 QString thisDir(dirs.takeLast());
185 if (thisDir.endsWith(QStringLiteral("*"))) {
186 qCInfo(KNEWSTUFFCORE) << "Directory entry" << thisDir
187 << "ends in a *, indicating this was installed from an archive - see Installation::archiveEntries";
188 thisDir.chop(1);
189 }
190
191 const QString currentPath = QDir::cleanPath(thisDir);
192 qCInfo(KNEWSTUFFCORE) << "Current path is" << currentPath;
193 if (!currentPath.startsWith(rootPath)) {
194 qCInfo(KNEWSTUFFCORE) << "Current path" << currentPath << "does not start with" << rootPath << "and should be ignored";
195 continue;
196 }
197
198 const QFileInfo current(currentPath);
199 qCInfo(KNEWSTUFFCORE) << "Current file info is" << current;
200 if (!current.isDir()) {
201 qCInfo(KNEWSTUFFCORE) << "Current path" << currentPath << "is not a directory, and should be ignored";
202 continue;
203 }
204
205 const QDir dir(currentPath);
206 if (dir.path() == (rootPath + dir.dirName())) {
207 qCDebug(KNEWSTUFFCORE) << "Found directory" << dir;
208 return dir;
209 }
210 }
211 qCWarning(KNEWSTUFFCORE) << "Failed to locate any shared installed directory in" << dirs << "and this is almost certainly very bad.";
212 return {};
213}
214
215static QString getAdoptionCommand(const QString &command, const KNSCore::Entry &entry, Installation *inst)
216{
217 auto adoption = command;
218 if (adoption.isEmpty()) {
219 return {};
220 }
221
222 const QLatin1String dirReplace("%d");
223 if (adoption.contains(dirReplace)) {
224 QString installPath = sharedDir(entry.installedFiles(), inst->targetInstallationPath()).path();
225 adoption.replace(dirReplace, KShell::quoteArg(installPath));
226 }
227
228 const QLatin1String fileReplace("%f");
229 if (adoption.contains(fileReplace)) {
230 if (entry.installedFiles().isEmpty()) {
231 qCWarning(KNEWSTUFFCORE) << "no installed files to adopt";
232 return {};
233 } else if (entry.installedFiles().count() != 1) {
234 qCWarning(KNEWSTUFFCORE) << "can only adopt one file, will be using the first" << entry.installedFiles().at(0);
235 }
236
237 adoption.replace(fileReplace, KShell::quoteArg(entry.installedFiles().at(0)));
238 }
239 return adoption;
240}
241
242Transaction::Transaction(const KNSCore::Entry &entry, EngineBase *engine)
243 : QObject(engine)
244 , d(new TransactionPrivate(entry, engine, this))
245{
246 connect(d->m_engine->d->installation, &Installation::signalEntryChanged, this, [this](const KNSCore::Entry &changedEntry) {
247 Q_EMIT signalEntryEvent(changedEntry, Entry::StatusChangedEvent);
248 d->m_engine->cache()->registerChangedEntry(changedEntry);
249 });
250 connect(d->m_engine->d->installation, &Installation::signalInstallationFailed, this, [this](const QString &message, const KNSCore::Entry &entry) {
251 if (entry == d->subject) {
252 Q_EMIT signalErrorCode(KNSCore::ErrorCode::InstallationError, message, {});
253 d->finish();
254 }
255 });
256}
257
258Transaction::~Transaction() = default;
259
260#if KNEWSTUFFCORE_ENABLE_DEPRECATED_SINCE(6, 9)
261Transaction *Transaction::install(EngineBase *engine, const KNSCore::Entry &_entry, int _linkId)
262{
263 return TransactionPrivate::createInstallTransaction(_entry, engine, _linkId);
264}
265#endif
266
268{
269 return TransactionPrivate::createInstallTransaction(_entry, engine, -1);
270}
271
272Transaction *Transaction::installLinkId(EngineBase *engine, const KNSCore::Entry &_entry, quint8 _linkId)
273{
274 return TransactionPrivate::createInstallTransaction(_entry, engine, _linkId);
275}
276
277void Transaction::downloadLinkLoaded(const KNSCore::Entry &entry)
278{
279 if (entry.status() == KNSCore::Entry::Updating) {
280 if (d->payloadToIdentify[entry].isEmpty()) {
281 // If there's nothing to identify, and we've arrived here, then we know what the payload is
282 qCDebug(KNEWSTUFFCORE) << "If there's nothing to identify, and we've arrived here, then we know what the payload is";
283 d->m_engine->d->installation->install(entry);
284 d->payloadToIdentify.remove(entry);
285 d->finish();
286 } else if (d->payloads[entry].count() < entry.downloadLinkCount()) {
287 // We've got more to get before we can attempt to identify anything, so fetch the next one...
288 qCDebug(KNEWSTUFFCORE) << "We've got more to get before we can attempt to identify anything, so fetch the next one...";
289 QStringList payloads = d->payloads[entry];
290 payloads << entry.payload();
291 d->payloads[entry] = payloads;
292 const auto &p = d->m_engine->d->providerCores.value(entry.providerId());
293 if (p) {
294 // ok, so this should definitely always work, but... safety first, kids!
295 p->d->base->loadPayloadLink(entry, payloads.count());
296 }
297 } else {
298 // We now have all the links, so let's try and identify the correct one...
299 qCDebug(KNEWSTUFFCORE) << "We now have all the links, so let's try and identify the correct one...";
300 QString identifiedLink;
301 const QString payloadToIdentify = d->payloadToIdentify[entry];
303 const QStringList &payloads = d->payloads[entry];
304
305 if (payloads.contains(payloadToIdentify)) {
306 // Simplest option, the link hasn't changed at all
307 qCDebug(KNEWSTUFFCORE) << "Simplest option, the link hasn't changed at all";
308 identifiedLink = payloadToIdentify;
309 } else {
310 // Next simplest option, filename is the same but in a different folder
311 qCDebug(KNEWSTUFFCORE) << "Next simplest option, filename is the same but in a different folder";
312 const QString fileName = payloadToIdentify.split(QChar::fromLatin1('/')).last();
313 for (const QString &payload : payloads) {
314 if (payload.endsWith(fileName)) {
315 identifiedLink = payload;
316 break;
317 }
318 }
319
320 // Possibly the payload itself is named differently (by a CDN, for example), but the link identifier is the same...
321 qCDebug(KNEWSTUFFCORE) << "Possibly the payload itself is named differently (by a CDN, for example), but the link identifier is the same...";
322 QStringList payloadNames;
323 for (const Entry::DownloadLinkInformation &downloadLink : downloadLinks) {
324 qCDebug(KNEWSTUFFCORE) << "Download link" << downloadLink.name << downloadLink.id << downloadLink.size << downloadLink.descriptionLink;
325 payloadNames << downloadLink.name;
326 if (downloadLink.name == fileName) {
327 identifiedLink = payloads[payloadNames.count() - 1];
328 qCDebug(KNEWSTUFFCORE) << "Found a suitable download link for" << fileName << "which should match" << identifiedLink;
329 }
330 }
331
332 if (identifiedLink.isEmpty()) {
333 // Least simple option, no match - ask the user to pick (and if we still haven't got one... that's us done, no installation)
334 qCDebug(KNEWSTUFFCORE)
335 << "Least simple option, no match - ask the user to pick (and if we still haven't got one... that's us done, no installation)";
336 auto question = std::make_unique<Question>(Question::SelectFromListQuestion);
337 question->setTitle(i18n("Pick Update Item"));
338 question->setQuestion(
339 i18n("Please pick the item from the list below which should be used to apply this update. We were unable to identify which item to "
340 "select, based on the original item, which was named %1",
341 fileName));
342 question->setList(payloadNames);
343 if (question->ask() == Question::OKResponse) {
344 identifiedLink = payloads.value(payloadNames.indexOf(question->response()));
345 }
346 }
347 }
348 if (!identifiedLink.isEmpty()) {
349 KNSCore::Entry theEntry(entry);
350 theEntry.setPayload(identifiedLink);
351 d->m_engine->d->installation->install(theEntry);
352 connect(d->m_engine->d->installation, &Installation::signalInstallationFinished, this, [this, entry](const KNSCore::Entry &finishedEntry) {
353 if (entry.uniqueId() == finishedEntry.uniqueId()) {
354 d->finish();
355 }
356 });
357 } else {
358 qCWarning(KNEWSTUFFCORE) << "We failed to identify a good link for updating" << entry.name() << "and are unable to perform the update";
359 KNSCore::Entry theEntry(entry);
360 theEntry.setStatus(KNSCore::Entry::Updateable);
362 Q_EMIT signalErrorCode(ErrorCode::InstallationError,
363 i18n("We failed to identify a good link for updating %1, and are unable to perform the update", entry.name()),
364 {entry.uniqueId()});
365 }
366 // As the serverside data may change before next time this is called, even in the same session,
367 // let's not make assumptions, and just get rid of this
368 d->payloads.remove(entry);
369 d->payloadToIdentify.remove(entry);
370 d->finish();
371 }
372 } else {
373 d->m_engine->d->installation->install(entry);
374 connect(d->m_engine->d->installation, &Installation::signalInstallationFinished, this, [this, entry](const KNSCore::Entry &finishedEntry) {
375 if (entry.uniqueId() == finishedEntry.uniqueId()) {
376 d->finish();
377 }
378 });
379 }
380}
381
383{
384 auto ret = new Transaction(_entry, engine);
385 const KNSCore::Entry::List list = ret->d->m_engine->cache()->registryForProvider(_entry.providerId());
386 // we have to use the cached entry here, not the entry from the provider
387 // since that does not contain the list of installed files
388 KNSCore::Entry actualEntryForUninstall;
389 for (const KNSCore::Entry &eInt : list) {
390 if (eInt.uniqueId() == _entry.uniqueId()) {
391 actualEntryForUninstall = eInt;
392 break;
393 }
394 }
395 if (!actualEntryForUninstall.isValid()) {
396 qCDebug(KNEWSTUFFCORE) << "could not find a cached entry with following id:" << _entry.uniqueId() << " -> using the non-cached version";
397 actualEntryForUninstall = _entry;
398 }
399
400 QTimer::singleShot(0, ret, [actualEntryForUninstall, _entry, ret] {
401 KNSCore::Entry entry = _entry;
402 entry.setStatus(KNSCore::Entry::Installing);
403
404 Entry actualEntryForUninstall2 = actualEntryForUninstall;
405 actualEntryForUninstall2.setStatus(KNSCore::Entry::Installing);
406 Q_EMIT ret->signalEntryEvent(entry, Entry::StatusChangedEvent);
407
408 // We connect to/forward the relevant signals
409 qCDebug(KNEWSTUFFCORE) << "about to uninstall entry " << entry.uniqueId();
410 ret->d->m_engine->d->installation->uninstall(actualEntryForUninstall2);
411
412 // FIXME: signalEntryEvent to uninstalled already happened in installation.cpp:584
413 // Update the correct entry
414 entry.setStatus(actualEntryForUninstall2.status());
415 Q_EMIT ret->signalEntryEvent(entry, Entry::StatusChangedEvent);
416
417 ret->d->finish();
418 });
419
420 return ret;
421}
422
424{
425 if (!engine->hasAdoptionCommand()) {
426 qCWarning(KNEWSTUFFCORE) << "no adoption command specified";
427 return nullptr;
428 }
429
430 auto ret = new Transaction(entry, engine);
431 const QString command = getAdoptionCommand(engine->d->adoptionCommand, entry, engine->d->installation);
432
433 QTimer::singleShot(0, ret, [command, entry, ret] {
434 QStringList split = KShell::splitArgs(command);
435 QProcess *process = new QProcess(ret);
436 process->setProgram(split.takeFirst());
437 process->setArguments(split);
438
440 // The debug output is too talkative to be useful
441 env.insert(QStringLiteral("QT_LOGGING_RULES"), QStringLiteral("*.debug=false"));
442 process->setProcessEnvironment(env);
443
444 process->start();
445
446 connect(process, &QProcess::finished, ret, [ret, process, entry, command](int exitCode) {
447 if (exitCode == 0) {
448 Q_EMIT ret->signalEntryEvent(entry, Entry::EntryEvent::AdoptedEvent);
449
450 // Handle error output as warnings if the process hasn't crashed
451 const QString stdErr = QString::fromLocal8Bit(process->readAllStandardError());
452 if (!stdErr.isEmpty()) {
453 Q_EMIT ret->signalMessage(stdErr);
454 }
455 } else {
456 const QString errorMsg = i18n("Failed to adopt '%1'\n%2", entry.name(), QString::fromLocal8Bit(process->readAllStandardError()));
457 Q_EMIT ret->signalErrorCode(KNSCore::ErrorCode::AdoptionError, errorMsg, QVariantList{command});
458 }
459 ret->d->finish();
460 });
461 });
462 return ret;
463}
464
466{
467 return d->m_finished;
468}
469
470#include "moc_transaction.cpp"
KNewStuff engine.
Definition enginebase.h:56
bool hasAdoptionCommand() const
Whether or not an adoption command exists for this engine.
KNewStuff data entry container.
Definition entry.h:48
QList< DownloadLinkInformation > downloadLinkInformationList() const
A list of downloadable data for this entry.
Definition entry.cpp:354
@ StatusChangedEvent
Used when an event's status is set (use Entry::status() to get the new status)
Definition entry.h:122
@ AdoptedEvent
Used when an entry has been successfully adopted (use this to determine whether a call to Engine::ado...
Definition entry.h:123
QStringList installedFiles() const
Retrieve the locally installed files.
Definition entry.cpp:339
QString payload() const
Retrieve the file name of the object.
Definition entry.cpp:186
void setStatus(KNSCore::Entry::Status status)
Sets the entry's status.
Definition entry.cpp:329
int downloadLinkCount() const
The number of available download options for this entry.
Definition entry.cpp:349
KNewStuff Transaction.
Definition transaction.h:38
static Transaction * install(EngineBase *engine, const Entry &entry, int linkId=1)
Performs an install on the given entry from the engine.
void signalEntryEvent(const KNSCore::Entry &entry, KNSCore::Entry::EntryEvent event)
Informs about how the entry has changed.
void signalErrorCode(KNSCore::ErrorCode::ErrorCode errorCode, const QString &message, const QVariant &metadata)
Fires in the case of any critical or serious errors, such as network or API problems.
static Transaction * installLatest(EngineBase *engine, const Entry &entry)
Performs an install of the latest version on the given entry from the engine.
static Transaction * installLinkId(EngineBase *engine, const Entry &entry, quint8 linkId)
Performs an install on the given entry from the engine.
static Transaction * adopt(EngineBase *engine, const Entry &entry)
Adopt the entry from engine using the adoption command.
static Transaction * uninstall(EngineBase *engine, const Entry &entry)
Uninstalls the given entry from the engine.
bool isFinished() const
QString i18n(const char *text, const TYPE &arg...)
KIOCORE_EXPORT QString number(KIO::filesize_t size)
KIOCORE_EXPORT QString dir(const QString &fileClass)
KCOREADDONS_EXPORT QStringList splitArgs(const QString &cmd, Options flags=NoOptions, Errors *err=nullptr)
KCOREADDONS_EXPORT QString quoteArg(const QString &arg)
QChar fromLatin1(char c)
QString cleanPath(const QString &path)
QString path() const const
const T * constData() const const
const_reference at(qsizetype i) const const
qsizetype count() const const
bool isEmpty() const const
T & last()
qsizetype size() const const
value_type takeFirst()
value_type takeLast()
bool contains(const Key &key) const const
bool isEmpty() const const
T & last()
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void finished(int exitCode, QProcess::ExitStatus exitStatus)
QByteArray readAllStandardError()
void setArguments(const QStringList &arguments)
void setProcessEnvironment(const QProcessEnvironment &environment)
void setProgram(const QString &program)
void start(OpenMode mode)
void insert(const QProcessEnvironment &e)
QProcessEnvironment systemEnvironment()
QString fromLocal8Bit(QByteArrayView str)
bool isEmpty() const const
bool isNull() const const
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
qsizetype indexOf(const QRegularExpression &re, qsizetype from) const const
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QVersionNumber fromString(QAnyStringView string, qsizetype *suffixIndex)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:20:03 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.