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"