File indexing completed on 2024-04-28 03:56:27

0001 /*
0002     SPDX-FileCopyrightText: 2023 Aleix Pol Gonzalez <aleixpol@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-or-later
0005 */
0006 
0007 #include "transaction.h"
0008 #include "enginebase.h"
0009 #include "enginebase_p.h"
0010 #include "provider.h"
0011 #include "question.h"
0012 
0013 #include <KLocalizedString>
0014 #include <KShell>
0015 #include <QDir>
0016 #include <QProcess>
0017 #include <QTimer>
0018 
0019 #include <knewstuffcore_debug.h>
0020 
0021 using namespace KNSCore;
0022 
0023 class KNSCore::TransactionPrivate
0024 {
0025 public:
0026     TransactionPrivate(const KNSCore::Entry &entry, EngineBase *engine, Transaction *q)
0027         : m_engine(engine)
0028         , q(q)
0029         , subject(entry)
0030     {
0031     }
0032 
0033     void finish()
0034     {
0035         m_finished = true;
0036         Q_EMIT q->finished();
0037         q->deleteLater();
0038     }
0039 
0040     EngineBase *const m_engine;
0041     Transaction *const q;
0042     bool m_finished = false;
0043     // 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
0044     // TODO KF6: Installed state needs to move onto a per-downloadlink basis rather than per-entry
0045     QMap<Entry, QStringList> payloads;
0046     QMap<Entry, QString> payloadToIdentify;
0047     const Entry subject;
0048 };
0049 
0050 /**
0051  * we look for the directory where all the resources got installed.
0052  * assuming it was extracted into a directory
0053  */
0054 static QDir sharedDir(QStringList dirs, QString rootPath)
0055 {
0056     // Ensure that rootPath definitely is a clean path with a slash at the end
0057     rootPath = QDir::cleanPath(rootPath) + QStringLiteral("/");
0058     qCInfo(KNEWSTUFFCORE) << Q_FUNC_INFO << dirs << rootPath;
0059     while (!dirs.isEmpty()) {
0060         QString thisDir(dirs.takeLast());
0061         if (thisDir.endsWith(QStringLiteral("*"))) {
0062             qCInfo(KNEWSTUFFCORE) << "Directory entry" << thisDir
0063                                   << "ends in a *, indicating this was installed from an archive - see Installation::archiveEntries";
0064             thisDir.chop(1);
0065         }
0066 
0067         const QString currentPath = QDir::cleanPath(thisDir);
0068         qCInfo(KNEWSTUFFCORE) << "Current path is" << currentPath;
0069         if (!currentPath.startsWith(rootPath)) {
0070             qCInfo(KNEWSTUFFCORE) << "Current path" << currentPath << "does not start with" << rootPath << "and should be ignored";
0071             continue;
0072         }
0073 
0074         const QFileInfo current(currentPath);
0075         qCInfo(KNEWSTUFFCORE) << "Current file info is" << current;
0076         if (!current.isDir()) {
0077             qCInfo(KNEWSTUFFCORE) << "Current path" << currentPath << "is not a directory, and should be ignored";
0078             continue;
0079         }
0080 
0081         const QDir dir(currentPath);
0082         if (dir.path() == (rootPath + dir.dirName())) {
0083             qCDebug(KNEWSTUFFCORE) << "Found directory" << dir;
0084             return dir;
0085         }
0086     }
0087     qCWarning(KNEWSTUFFCORE) << "Failed to locate any shared installed directory in" << dirs << "and this is almost certainly very bad.";
0088     return {};
0089 }
0090 
0091 static QString getAdoptionCommand(const QString &command, const KNSCore::Entry &entry, Installation *inst)
0092 {
0093     auto adoption = command;
0094     if (adoption.isEmpty()) {
0095         return {};
0096     }
0097 
0098     const QLatin1String dirReplace("%d");
0099     if (adoption.contains(dirReplace)) {
0100         QString installPath = sharedDir(entry.installedFiles(), inst->targetInstallationPath()).path();
0101         adoption.replace(dirReplace, KShell::quoteArg(installPath));
0102     }
0103 
0104     const QLatin1String fileReplace("%f");
0105     if (adoption.contains(fileReplace)) {
0106         if (entry.installedFiles().isEmpty()) {
0107             qCWarning(KNEWSTUFFCORE) << "no installed files to adopt";
0108             return {};
0109         } else if (entry.installedFiles().count() != 1) {
0110             qCWarning(KNEWSTUFFCORE) << "can only adopt one file, will be using the first" << entry.installedFiles().at(0);
0111         }
0112 
0113         adoption.replace(fileReplace, KShell::quoteArg(entry.installedFiles().at(0)));
0114     }
0115     return adoption;
0116 }
0117 
0118 Transaction::Transaction(const KNSCore::Entry &entry, EngineBase *engine)
0119     : QObject(engine)
0120     , d(new TransactionPrivate(entry, engine, this))
0121 {
0122     connect(d->m_engine->d->installation, &Installation::signalEntryChanged, this, [this](const KNSCore::Entry &changedEntry) {
0123         Q_EMIT signalEntryEvent(changedEntry, Entry::StatusChangedEvent);
0124         d->m_engine->cache()->registerChangedEntry(changedEntry);
0125     });
0126     connect(d->m_engine->d->installation, &Installation::signalInstallationFailed, this, [this](const QString &message, const KNSCore::Entry &entry) {
0127         if (entry == d->subject) {
0128             Q_EMIT signalErrorCode(KNSCore::ErrorCode::InstallationError, message, {});
0129             d->finish();
0130         }
0131     });
0132 }
0133 
0134 Transaction::~Transaction() = default;
0135 
0136 Transaction *Transaction::install(EngineBase *engine, const KNSCore::Entry &_entry, int _linkId)
0137 {
0138     auto ret = new Transaction(_entry, engine);
0139     connect(engine->d->installation, &Installation::signalInstallationError, ret, [ret, _entry](const QString &msg, const KNSCore::Entry &entry) {
0140         if (_entry.uniqueId() == entry.uniqueId()) {
0141             Q_EMIT ret->signalErrorCode(KNSCore::ErrorCode::InstallationError, msg, {});
0142         }
0143     });
0144 
0145     QTimer::singleShot(0, ret, [_entry, ret, _linkId, engine] {
0146         int linkId = _linkId;
0147         KNSCore::Entry entry = _entry;
0148         if (entry.downloadLinkCount() == 0 && entry.payload().isEmpty()) {
0149             // Turns out this happens sometimes, so we should deal with that and spit out an error
0150             qCDebug(KNEWSTUFFCORE) << "There were no downloadlinks defined in the entry we were just asked to update: " << entry.uniqueId() << "on provider"
0151                                    << entry.providerId();
0152             Q_EMIT ret->signalErrorCode(
0153                 KNSCore::ErrorCode::InstallationError,
0154                 i18n("Could not perform an installation of the entry %1 as it does not have any downloadable items defined. Please contact the "
0155                      "author so they can fix this.",
0156                      entry.name()),
0157                 entry.uniqueId());
0158             ret->d->finish();
0159         } else {
0160             if (entry.status() == KNSCore::Entry::Updateable) {
0161                 entry.setStatus(KNSCore::Entry::Updating);
0162             } else {
0163                 entry.setStatus(KNSCore::Entry::Installing);
0164             }
0165             Q_EMIT ret->signalEntryEvent(entry, Entry::StatusChangedEvent);
0166 
0167             qCDebug(KNEWSTUFFCORE) << "Install " << entry.name() << " from: " << entry.providerId();
0168             QSharedPointer<Provider> p = engine->d->providers.value(entry.providerId());
0169             if (p) {
0170                 connect(p.data(), &Provider::payloadLinkLoaded, ret, &Transaction::downloadLinkLoaded);
0171                 // If linkId is -1, assume that it's an update and that we don't know what to update
0172                 if (entry.status() == KNSCore::Entry::Updating && linkId == -1) {
0173                     if (entry.downloadLinkCount() == 1 || !entry.payload().isEmpty()) {
0174                         // If there is only one downloadable item (which also includes a predefined payload name), then we can fairly safely assume that's what
0175                         // we're wanting to update, meaning we can bypass some of the more expensive operations in downloadLinkLoaded
0176                         qCDebug(KNEWSTUFFCORE) << "Just the one download link, so let's use that";
0177                         ret->d->payloadToIdentify[entry] = QString{};
0178                         linkId = 1;
0179                     } else {
0180                         qCDebug(KNEWSTUFFCORE) << "Try and identify a download link to use from a total of" << entry.downloadLinkCount();
0181                         // While this seems silly, the payload gets reset when fetching the new download link information
0182                         ret->d->payloadToIdentify[entry] = entry.payload();
0183                         // Drop a fresh list in place so we've got something to work with when we get the links
0184                         ret->d->payloads[entry] = QStringList{};
0185                         linkId = 1;
0186                     }
0187                 } else {
0188                     qCDebug(KNEWSTUFFCORE) << "Link ID already known" << linkId;
0189                     // If there is no payload to identify, we will assume the payload is already known and just use that
0190                     ret->d->payloadToIdentify[entry] = QString{};
0191                 }
0192 
0193                 p->loadPayloadLink(entry, linkId);
0194 
0195                 ret->d->m_finished = false;
0196                 ret->d->m_engine->updateStatus();
0197             }
0198         }
0199     });
0200     return ret;
0201 }
0202 
0203 void Transaction::downloadLinkLoaded(const KNSCore::Entry &entry)
0204 {
0205     if (entry.status() == KNSCore::Entry::Updating) {
0206         if (d->payloadToIdentify[entry].isEmpty()) {
0207             // If there's nothing to identify, and we've arrived here, then we know what the payload is
0208             qCDebug(KNEWSTUFFCORE) << "If there's nothing to identify, and we've arrived here, then we know what the payload is";
0209             d->m_engine->d->installation->install(entry);
0210             d->payloadToIdentify.remove(entry);
0211             d->finish();
0212         } else if (d->payloads[entry].count() < entry.downloadLinkCount()) {
0213             // We've got more to get before we can attempt to identify anything, so fetch the next one...
0214             qCDebug(KNEWSTUFFCORE) << "We've got more to get before we can attempt to identify anything, so fetch the next one...";
0215             QStringList payloads = d->payloads[entry];
0216             payloads << entry.payload();
0217             d->payloads[entry] = payloads;
0218             QSharedPointer<Provider> p = d->m_engine->d->providers.value(entry.providerId());
0219             if (p) {
0220                 // ok, so this should definitely always work, but... safety first, kids!
0221                 p->loadPayloadLink(entry, payloads.count());
0222             }
0223         } else {
0224             // We now have all the links, so let's try and identify the correct one...
0225             qCDebug(KNEWSTUFFCORE) << "We now have all the links, so let's try and identify the correct one...";
0226             QString identifiedLink;
0227             const QString payloadToIdentify = d->payloadToIdentify[entry];
0228             const QList<Entry::DownloadLinkInformation> downloadLinks = entry.downloadLinkInformationList();
0229             const QStringList &payloads = d->payloads[entry];
0230 
0231             if (payloads.contains(payloadToIdentify)) {
0232                 // Simplest option, the link hasn't changed at all
0233                 qCDebug(KNEWSTUFFCORE) << "Simplest option, the link hasn't changed at all";
0234                 identifiedLink = payloadToIdentify;
0235             } else {
0236                 // Next simplest option, filename is the same but in a different folder
0237                 qCDebug(KNEWSTUFFCORE) << "Next simplest option, filename is the same but in a different folder";
0238                 const QString fileName = payloadToIdentify.split(QChar::fromLatin1('/')).last();
0239                 for (const QString &payload : payloads) {
0240                     if (payload.endsWith(fileName)) {
0241                         identifiedLink = payload;
0242                         break;
0243                     }
0244                 }
0245 
0246                 // Possibly the payload itself is named differently (by a CDN, for example), but the link identifier is the same...
0247                 qCDebug(KNEWSTUFFCORE) << "Possibly the payload itself is named differently (by a CDN, for example), but the link identifier is the same...";
0248                 QStringList payloadNames;
0249                 for (const Entry::DownloadLinkInformation &downloadLink : downloadLinks) {
0250                     qCDebug(KNEWSTUFFCORE) << "Download link" << downloadLink.name << downloadLink.id << downloadLink.size << downloadLink.descriptionLink;
0251                     payloadNames << downloadLink.name;
0252                     if (downloadLink.name == fileName) {
0253                         identifiedLink = payloads[payloadNames.count() - 1];
0254                         qCDebug(KNEWSTUFFCORE) << "Found a suitable download link for" << fileName << "which should match" << identifiedLink;
0255                     }
0256                 }
0257 
0258                 if (identifiedLink.isEmpty()) {
0259                     // Least simple option, no match - ask the user to pick (and if we still haven't got one... that's us done, no installation)
0260                     qCDebug(KNEWSTUFFCORE)
0261                         << "Least simple option, no match - ask the user to pick (and if we still haven't got one... that's us done, no installation)";
0262                     auto question = std::make_unique<Question>(Question::SelectFromListQuestion);
0263                     question->setTitle(i18n("Pick Update Item"));
0264                     question->setQuestion(
0265                         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 "
0266                              "select, based on the original item, which was named %1",
0267                              fileName));
0268                     question->setList(payloadNames);
0269                     if (question->ask() == Question::OKResponse) {
0270                         identifiedLink = payloads.value(payloadNames.indexOf(question->response()));
0271                     }
0272                 }
0273             }
0274             if (!identifiedLink.isEmpty()) {
0275                 KNSCore::Entry theEntry(entry);
0276                 theEntry.setPayload(identifiedLink);
0277                 d->m_engine->d->installation->install(theEntry);
0278                 connect(d->m_engine->d->installation, &Installation::signalInstallationFinished, this, [this, entry](const KNSCore::Entry &finishedEntry) {
0279                     if (entry.uniqueId() == finishedEntry.uniqueId()) {
0280                         d->finish();
0281                     }
0282                 });
0283             } else {
0284                 qCWarning(KNEWSTUFFCORE) << "We failed to identify a good link for updating" << entry.name() << "and are unable to perform the update";
0285                 KNSCore::Entry theEntry(entry);
0286                 theEntry.setStatus(KNSCore::Entry::Updateable);
0287                 Q_EMIT signalEntryEvent(theEntry, Entry::StatusChangedEvent);
0288                 Q_EMIT signalErrorCode(ErrorCode::InstallationError,
0289                                        i18n("We failed to identify a good link for updating %1, and are unable to perform the update", entry.name()),
0290                                        {entry.uniqueId()});
0291             }
0292             // As the serverside data may change before next time this is called, even in the same session,
0293             // let's not make assumptions, and just get rid of this
0294             d->payloads.remove(entry);
0295             d->payloadToIdentify.remove(entry);
0296             d->finish();
0297         }
0298     } else {
0299         d->m_engine->d->installation->install(entry);
0300         connect(d->m_engine->d->installation, &Installation::signalInstallationFinished, this, [this, entry](const KNSCore::Entry &finishedEntry) {
0301             if (entry.uniqueId() == finishedEntry.uniqueId()) {
0302                 d->finish();
0303             }
0304         });
0305     }
0306 }
0307 
0308 Transaction *Transaction::uninstall(EngineBase *engine, const KNSCore::Entry &_entry)
0309 {
0310     auto ret = new Transaction(_entry, engine);
0311     const KNSCore::Entry::List list = ret->d->m_engine->cache()->registryForProvider(_entry.providerId());
0312     // we have to use the cached entry here, not the entry from the provider
0313     // since that does not contain the list of installed files
0314     KNSCore::Entry actualEntryForUninstall;
0315     for (const KNSCore::Entry &eInt : list) {
0316         if (eInt.uniqueId() == _entry.uniqueId()) {
0317             actualEntryForUninstall = eInt;
0318             break;
0319         }
0320     }
0321     if (!actualEntryForUninstall.isValid()) {
0322         qCDebug(KNEWSTUFFCORE) << "could not find a cached entry with following id:" << _entry.uniqueId() << " ->  using the non-cached version";
0323         actualEntryForUninstall = _entry;
0324     }
0325 
0326     QTimer::singleShot(0, ret, [actualEntryForUninstall, _entry, ret] {
0327         KNSCore::Entry entry = _entry;
0328         entry.setStatus(KNSCore::Entry::Installing);
0329 
0330         Entry actualEntryForUninstall2 = actualEntryForUninstall;
0331         actualEntryForUninstall2.setStatus(KNSCore::Entry::Installing);
0332         Q_EMIT ret->signalEntryEvent(entry, Entry::StatusChangedEvent);
0333 
0334         // We connect to/forward the relevant signals
0335         qCDebug(KNEWSTUFFCORE) << "about to uninstall entry " << entry.uniqueId();
0336         ret->d->m_engine->d->installation->uninstall(actualEntryForUninstall2);
0337 
0338         ret->d->finish();
0339     });
0340     return ret;
0341 }
0342 
0343 Transaction *Transaction::adopt(EngineBase *engine, const Entry &entry)
0344 {
0345     if (!engine->hasAdoptionCommand()) {
0346         qCWarning(KNEWSTUFFCORE) << "no adoption command specified";
0347         return nullptr;
0348     }
0349 
0350     auto ret = new Transaction(entry, engine);
0351     const QString command = getAdoptionCommand(engine->d->adoptionCommand, entry, engine->d->installation);
0352 
0353     QTimer::singleShot(0, ret, [command, entry, ret] {
0354         QStringList split = KShell::splitArgs(command);
0355         QProcess *process = new QProcess(ret);
0356         process->setProgram(split.takeFirst());
0357         process->setArguments(split);
0358 
0359         QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
0360         // The debug output is too talkative to be useful
0361         env.insert(QStringLiteral("QT_LOGGING_RULES"), QStringLiteral("*.debug=false"));
0362         process->setProcessEnvironment(env);
0363 
0364         process->start();
0365 
0366         connect(process, &QProcess::finished, ret, [ret, process, entry, command](int exitCode) {
0367             if (exitCode == 0) {
0368                 Q_EMIT ret->signalEntryEvent(entry, Entry::EntryEvent::AdoptedEvent);
0369 
0370                 // Handle error output as warnings if the process hasn't crashed
0371                 const QString stdErr = QString::fromLocal8Bit(process->readAllStandardError());
0372                 if (!stdErr.isEmpty()) {
0373                     Q_EMIT ret->signalMessage(stdErr);
0374                 }
0375             } else {
0376                 const QString errorMsg = i18n("Failed to adopt '%1'\n%2", entry.name(), QString::fromLocal8Bit(process->readAllStandardError()));
0377                 Q_EMIT ret->signalErrorCode(KNSCore::ErrorCode::AdoptionError, errorMsg, QVariantList{command});
0378             }
0379             ret->d->finish();
0380         });
0381     });
0382     return ret;
0383 }
0384 
0385 bool Transaction::isFinished() const
0386 {
0387     return d->m_finished;
0388 }
0389 
0390 #include "moc_transaction.cpp"