File indexing completed on 2024-11-17 04:55:40
0001 /* 0002 * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez <aleixpol@blue-systems.com> 0003 * 0004 * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0005 */ 0006 0007 // Qt includes 0008 #include <QDebug> 0009 #include <QDir> 0010 #include <QDirIterator> 0011 #include <QFileInfo> 0012 #include <QStandardPaths> 0013 #include <QTimer> 0014 0015 // KDE includes 0016 #include <KConfig> 0017 #include <KConfigGroup> 0018 #include <KLocalizedString> 0019 #include <KNSCore/Provider> 0020 #include <KNSCore/Question> 0021 #include <KNSCore/QuestionManager> 0022 #include <KNSCore/ResultsStream> 0023 0024 // DiscoverCommon includes 0025 #include "Category/Category.h" 0026 #include "Transaction/Transaction.h" 0027 #include "Transaction/TransactionModel.h" 0028 0029 // Own includes 0030 #include "KNSBackend.h" 0031 #include "KNSResource.h" 0032 #include "KNSReviews.h" 0033 #include "KNSTransaction.h" 0034 #include "utils.h" 0035 #include <resources/StandardBackendUpdater.h> 0036 0037 using namespace Qt::StringLiterals; 0038 0039 static const int ENGINE_PAGE_SIZE = 100; 0040 0041 class KNSBackendFactory : public AbstractResourcesBackendFactory 0042 { 0043 Q_OBJECT 0044 Q_PLUGIN_METADATA(IID "org.kde.muon.AbstractResourcesBackendFactory") 0045 Q_INTERFACES(AbstractResourcesBackendFactory) 0046 public: 0047 KNSBackendFactory() 0048 { 0049 connect(KNSCore::QuestionManager::instance(), &KNSCore::QuestionManager::askQuestion, this, [](KNSCore::Question *question) { 0050 const auto transactions = TransactionModel::global()->transactions(); 0051 for (auto t : transactions) { 0052 const auto transaction = dynamic_cast<KNSTransaction *>(t); 0053 if (!transaction) { 0054 continue; 0055 } 0056 0057 if (question->entry().uniqueId() == transaction->uniqueId()) { 0058 switch (question->questionType()) { 0059 case KNSCore::Question::ContinueCancelQuestion: 0060 transaction->addQuestion(question); 0061 return; 0062 default: 0063 transaction->passiveMessage(i18n("Unsupported question:\n%1", question->question())); 0064 question->setResponse(KNSCore::Question::InvalidResponse); 0065 transaction->setStatus(Transaction::CancelledStatus); 0066 break; 0067 } 0068 return; 0069 } 0070 } 0071 qWarning() << "Question for unknown transaction" << question->question() << question->questionType(); 0072 question->setResponse(KNSCore::Question::InvalidResponse); 0073 }); 0074 } 0075 0076 QVector<AbstractResourcesBackend *> newInstance(QObject *parent, const QString & /*name*/) const override 0077 { 0078 QVector<AbstractResourcesBackend *> ret; 0079 const QStringList availableConfigFiles = KNSCore::EngineBase::availableConfigFiles(); 0080 for (const QString &configFile : availableConfigFiles) { 0081 auto bk = new KNSBackend(parent, QStringLiteral("plasma"), configFile); 0082 if (bk->isValid()) 0083 ret += bk; 0084 else 0085 delete bk; 0086 } 0087 return ret; 0088 } 0089 }; 0090 0091 class KNSResultsStream : public ResultsStream 0092 { 0093 Q_OBJECT 0094 public: 0095 KNSResultsStream(KNSBackend *backend, const QString &objectName) 0096 : ResultsStream(objectName) 0097 , m_backend(backend) 0098 { 0099 } 0100 0101 void setRequest(const KNSCore::Provider::SearchRequest &request) 0102 { 0103 KNSCore::ResultsStream *job = m_backend->engine()->search(request); 0104 connect(job, &KNSCore::ResultsStream::entriesFound, this, &KNSResultsStream::addEntries); 0105 connect(job, &KNSCore::ResultsStream::finished, this, &KNSResultsStream::finish); 0106 connect(this, &ResultsStream::fetchMore, job, &KNSCore::ResultsStream::fetchMore); 0107 job->fetch(); 0108 } 0109 0110 void addEntries(const KNSCore::Entry::List &entries) 0111 { 0112 const auto res = kTransform<QList<StreamResult>>(entries, [this](const auto &entry) { 0113 return StreamResult{m_backend->resourceForEntry(entry), 0}; 0114 }); 0115 Q_EMIT resourcesFound(res); 0116 } 0117 0118 private: 0119 KNSBackend *const m_backend; 0120 }; 0121 0122 KNSBackend::KNSBackend(QObject *parent, const QString &iconName, const QString &knsrc) 0123 : AbstractResourcesBackend(parent) 0124 , m_fetching(false) 0125 , m_isValid(true) 0126 , m_reviews(new KNSReviews(this)) 0127 , m_name(knsrc) 0128 , m_iconName(iconName) 0129 , m_updater(new StandardBackendUpdater(this)) 0130 { 0131 const QString fileName = QFileInfo(m_name).fileName(); 0132 setName(fileName); 0133 setObjectName(knsrc); 0134 0135 const KConfig conf(m_name, KConfig::SimpleConfig); 0136 const bool hasVersionlessGrp = conf.hasGroup(u"KNewStuff"_s); 0137 if (!conf.hasGroup(u"KNewStuff3"_s) && !hasVersionlessGrp) { 0138 markInvalid(QStringLiteral("Config group not found! Check your KNSCore installation.")); 0139 return; 0140 } 0141 0142 m_categories = QStringList{fileName}; 0143 0144 const KConfigGroup group = hasVersionlessGrp ? conf.group(u"KNewStuff"_s) : conf.group(u"KNewStuff3"_s); 0145 m_extends = group.readEntry("Extends", QStringList()); 0146 0147 setFetching(true); 0148 0149 // This ensures we have something to track when checking after the initialization timeout 0150 connect(this, &KNSBackend::initialized, this, [this]() { 0151 m_initialized = true; 0152 }); 0153 // If we have not initialized in 60 seconds, consider this KNS backend invalid 0154 QTimer::singleShot(60000, this, [this]() { 0155 if (!m_initialized) { 0156 markInvalid(i18n("Backend %1 took too long to initialize", m_displayName)); 0157 } 0158 }); 0159 0160 const CategoryFilter filter = {CategoryFilter::CategoryNameFilter, fileName}; 0161 const QSet<QString> backendName = {name()}; 0162 m_displayName = group.readEntry("Name", QString()); 0163 if (m_displayName.isEmpty()) { 0164 m_displayName = fileName.mid(0, fileName.indexOf(QLatin1Char('.'))); 0165 m_displayName[0] = m_displayName[0].toUpper(); 0166 } 0167 m_hasApplications = group.readEntry<bool>("X-Discover-HasApplications", false); 0168 0169 const QStringList cats = group.readEntry<QStringList>("Categories", QStringList{}); 0170 QVector<Category *> categories; 0171 if (cats.count() > 1) { 0172 m_categories += cats; 0173 for (const auto &cat : cats) { 0174 if (m_hasApplications) 0175 categories << new Category(cat, QStringLiteral("applications-other"), {CategoryFilter::CategoryNameFilter, cat}, backendName, {}, true); 0176 else 0177 categories << new Category(cat, QStringLiteral("plasma"), {CategoryFilter::CategoryNameFilter, cat}, backendName, {}, true); 0178 } 0179 } 0180 0181 QVector<Category *> topCategories{categories}; 0182 for (const auto &cat : std::as_const(categories)) { 0183 const QString catName = cat->name().append(QLatin1Char('/')); 0184 for (const auto &potentialSubCat : std::as_const(categories)) { 0185 if (potentialSubCat->name().startsWith(catName)) { 0186 cat->addSubcategory(potentialSubCat); 0187 topCategories.removeOne(potentialSubCat); 0188 } 0189 } 0190 } 0191 0192 m_engine = new KNSCore::EngineBase(this); 0193 connect(m_engine, &KNSCore::EngineBase::signalErrorCode, this, &KNSBackend::slotErrorCode); 0194 connect(m_engine, &KNSCore::EngineBase::providersChanged, this, [this] { 0195 setFetching(false); 0196 }); 0197 0198 connect(m_engine, 0199 &KNSCore::EngineBase::signalCategoriesMetadataLoded, 0200 this, 0201 [categories](const QList<KNSCore::Provider::CategoryMetadata> &categoryMetadatas) { 0202 for (const KNSCore::Provider::CategoryMetadata &category : categoryMetadatas) { 0203 for (Category *cat : std::as_const(categories)) { 0204 if (cat->matchesCategoryName(category.name)) { 0205 cat->setName(category.displayName); 0206 break; 0207 } 0208 } 0209 } 0210 }); 0211 m_engine->init(m_name); 0212 0213 if (m_hasApplications) { 0214 auto actualCategory = new Category(m_displayName, QStringLiteral("applications-other"), filter, backendName, topCategories, false); 0215 auto applicationCategory = new Category(i18n("Applications"), // 0216 QStringLiteral("applications-internet"), 0217 filter, 0218 backendName, 0219 {actualCategory}, 0220 false); 0221 const QVector<CategoryFilter> filters = {{CategoryFilter::CategoryNameFilter, QLatin1String("Application")}, filter}; 0222 applicationCategory->setFilter({CategoryFilter::AndFilter, filters}); 0223 m_categories.append(applicationCategory->name()); 0224 m_rootCategories = {applicationCategory}; 0225 // Make sure we filter out any apps which won't run on the current system architecture 0226 QStringList tagFilter = m_engine->tagFilter(); 0227 if (QSysInfo::currentCpuArchitecture() == QLatin1String("arm")) { 0228 tagFilter << QLatin1String("application##architecture==armhf"); 0229 } else if (QSysInfo::currentCpuArchitecture() == QLatin1String("arm64")) { 0230 tagFilter << QLatin1String("application##architecture==arm64"); 0231 } else if (QSysInfo::currentCpuArchitecture() == QLatin1String("i386")) { 0232 tagFilter << QLatin1String("application##architecture==x86"); 0233 } else if (QSysInfo::currentCpuArchitecture() == QLatin1String("ia64")) { 0234 tagFilter << QLatin1String("application##architecture==x86-64"); 0235 } else if (QSysInfo::currentCpuArchitecture() == QLatin1String("x86_64")) { 0236 tagFilter << QLatin1String("application##architecture==x86"); 0237 tagFilter << QLatin1String("application##architecture==x86-64"); 0238 } 0239 m_engine->setTagFilter(tagFilter); 0240 } else { 0241 static const QSet<QString> knsrcPlasma = { 0242 QStringLiteral("aurorae.knsrc"), QStringLiteral("icons.knsrc"), 0243 QStringLiteral("kfontinst.knsrc"), QStringLiteral("lookandfeel.knsrc"), 0244 QStringLiteral("plasma-themes.knsrc"), QStringLiteral("plasmoids.knsrc"), 0245 QStringLiteral("wallpaper.knsrc"), QStringLiteral("wallpaper-mobile.knsrc"), 0246 QStringLiteral("xcursor.knsrc"), 0247 0248 QStringLiteral("cgcgtk3.knsrc"), QStringLiteral("cgcicon.knsrc"), 0249 QStringLiteral("cgctheme.knsrc"), // GTK integration 0250 QStringLiteral("kwinswitcher.knsrc"), QStringLiteral("kwineffect.knsrc"), 0251 QStringLiteral("kwinscripts.knsrc"), // KWin 0252 QStringLiteral("comic.knsrc"), QStringLiteral("colorschemes.knsrc"), 0253 QStringLiteral("emoticons.knsrc"), QStringLiteral("plymouth.knsrc"), 0254 QStringLiteral("sddmtheme.knsrc"), QStringLiteral("wallpaperplugin.knsrc"), 0255 QStringLiteral("ksplash.knsrc"), QStringLiteral("window-decorations.knsrc"), 0256 }; 0257 const auto iconName = knsrcPlasma.contains(fileName) ? QStringLiteral("plasma") : QStringLiteral("applications-other"); 0258 auto actualCategory = new Category(m_displayName, iconName, filter, backendName, categories, true); 0259 actualCategory->setParent(this); 0260 0261 const auto topLevelName = knsrcPlasma.contains(fileName) ? i18n("Plasma Addons") : i18n("Application Addons"); 0262 auto addonsCategory = new Category(topLevelName, iconName, filter, backendName, {actualCategory}, true); 0263 m_rootCategories = {addonsCategory}; 0264 } 0265 0266 connect(m_updater, &StandardBackendUpdater::updatesCountChanged, this, &KNSBackend::updatesCountChanged); 0267 } 0268 0269 KNSBackend::~KNSBackend() 0270 { 0271 qDeleteAll(m_rootCategories); 0272 } 0273 0274 void KNSBackend::markInvalid(const QString &message) 0275 { 0276 m_rootCategories.clear(); 0277 qWarning() << "invalid kns backend!" << m_name << "because:" << message; 0278 m_isValid = false; 0279 setFetching(false); 0280 Q_EMIT initialized(); 0281 } 0282 0283 void KNSBackend::checkForUpdates() 0284 { 0285 AbstractResourcesBackend::Filters filter; 0286 filter.state = AbstractResource::Upgradeable; 0287 search(filter); 0288 } 0289 0290 void KNSBackend::setFetching(bool f) 0291 { 0292 if (m_fetching != f) { 0293 m_fetching = f; 0294 Q_EMIT fetchingChanged(); 0295 0296 if (!m_fetching) { 0297 Q_EMIT initialized(); 0298 } 0299 } 0300 } 0301 0302 bool KNSBackend::isValid() const 0303 { 0304 return m_isValid; 0305 } 0306 0307 KNSResource *KNSBackend::resourceForEntry(const KNSCore::Entry &entry) 0308 { 0309 KNSResource *r = static_cast<KNSResource *>(m_resourcesByName.value(entry.uniqueId())); 0310 if (!r) { 0311 QStringList categories{name()}; 0312 if (!m_rootCategories.isEmpty()) { 0313 categories << m_rootCategories.first()->name(); 0314 } 0315 const auto cats = m_engine->categoriesMetadata(); 0316 const int catIndex = kIndexOf(cats, [&entry](const KNSCore::Provider::CategoryMetadata &cat) { 0317 return entry.category() == cat.id; 0318 }); 0319 if (catIndex > -1) { 0320 categories << cats.at(catIndex).name; 0321 } 0322 if (m_hasApplications) { 0323 categories << QLatin1String("Application"); 0324 } 0325 r = new KNSResource(entry, categories, this); 0326 m_resourcesByName.insert(entry.uniqueId(), r); 0327 } else { 0328 r->setEntry(entry); 0329 } 0330 return r; 0331 } 0332 0333 void KNSBackend::statusChanged(const KNSCore::Entry &entry) 0334 { 0335 resourceForEntry(entry); 0336 } 0337 0338 void KNSBackend::slotErrorCode(const KNSCore::ErrorCode::ErrorCode &errorCode, const QString &message, const QVariant &metadata) 0339 { 0340 QString error = message; 0341 qWarning() << "KNS error in" << m_displayName << ":" << errorCode << message << metadata; 0342 bool invalidFile = false; 0343 switch (errorCode) { 0344 case KNSCore::ErrorCode::UnknownError: 0345 // This is not supposed to be hit, of course, but any error coming to this point should be non-critical and safely ignored. 0346 break; 0347 case KNSCore::ErrorCode::NetworkError: 0348 // If we have a network error, we need to tell the user about it. This is almost always fatal, so mark invalid and tell the user. 0349 error = i18n("Network error in backend %1: %2", m_displayName, metadata.toInt()); 0350 markInvalid(error); 0351 invalidFile = true; 0352 break; 0353 case KNSCore::ErrorCode::OcsError: 0354 if (metadata.toInt() == 200) { 0355 // Too many requests, try again in a couple of minutes - perhaps we can simply postpone it automatically, and give a message? 0356 error = i18n("Too many requests sent to the server for backend %1. Please try again in a few minutes.", m_displayName); 0357 } else { 0358 // Unknown API error, usually something critical, mark as invalid and cry a lot 0359 error = i18n("Invalid %1 backend, contact your distributor.", m_displayName); 0360 markInvalid(error); 0361 invalidFile = true; 0362 } 0363 break; 0364 case KNSCore::ErrorCode::ConfigFileError: 0365 error = i18n("Invalid %1 backend, contact your distributor.", m_displayName); 0366 markInvalid(error); 0367 invalidFile = true; 0368 break; 0369 case KNSCore::ErrorCode::ProviderError: 0370 error = i18n("Invalid %1 backend, contact your distributor.", m_displayName); 0371 markInvalid(error); 0372 invalidFile = true; 0373 break; 0374 case KNSCore::ErrorCode::InstallationError: { 0375 KNSResource *r = static_cast<KNSResource *>(m_resourcesByName.value(metadata.toString())); 0376 if (r) { 0377 // If the following is true, then we can safely assume that the entry was 0378 // attempted updated, but the update was aborted. 0379 // Specifically, we can also likely expect that the update failed because 0380 // KNSCore::Engine was unable to deduce which payload to use (which will 0381 // happen when an entry has more than one payload, and none of those match 0382 // the name of the originally downloaded file). 0383 // We cannot complete this in Discover (as we've no way to forward that 0384 // query to the user) but we can give them an idea of how to deal with the 0385 // situation some other way. 0386 // TODO: Once Discover has a way to forward queries to the user from transactions, this likely will no longer be needed 0387 if (r->entry().status() == KNSCore::Entry::Updateable) { 0388 error = i18n( 0389 "Unable to complete the update of %1. You can try and perform this action through the Get Hot New Stuff dialog, which grants tighter " 0390 "control. The reported error was:\n%2", 0391 r->name(), 0392 message); 0393 } 0394 } 0395 break; 0396 } 0397 case KNSCore::ErrorCode::ImageError: 0398 // Image fetching errors are not critical as such, but may lead to weird layout issues, might want handling... 0399 error = i18n("Could not fetch screenshot for the entry %1 in backend %2", metadata.toList().at(0).toString(), m_displayName); 0400 break; 0401 default: 0402 // Having handled all current error values, we should by all rights never arrive here, but for good order and future safety... 0403 error = i18n("Unhandled error in %1 backend. Contact your distributor.", m_displayName); 0404 break; 0405 } 0406 qWarning() << "kns error" << objectName() << error; 0407 if (!invalidFile) 0408 Q_EMIT passiveMessage(i18n("%1: %2", name(), error)); 0409 } 0410 0411 void KNSBackend::slotEntryEvent(const KNSCore::Entry &entry, KNSCore::Entry::EntryEvent event) 0412 { 0413 switch (event) { 0414 case KNSCore::Entry::StatusChangedEvent: 0415 statusChanged(entry); 0416 break; 0417 case KNSCore::Entry::DetailsLoadedEvent: 0418 detailsLoaded(entry); 0419 break; 0420 case KNSCore::Entry::AdoptedEvent: 0421 case KNSCore::Entry::UnknownEvent: 0422 default: 0423 break; 0424 } 0425 } 0426 0427 Transaction *KNSBackend::removeApplication(AbstractResource *app) 0428 { 0429 auto res = qobject_cast<KNSResource *>(app); 0430 return new KNSTransaction(this, res, Transaction::RemoveRole); 0431 } 0432 0433 Transaction *KNSBackend::installApplication(AbstractResource *app) 0434 { 0435 auto res = qobject_cast<KNSResource *>(app); 0436 return new KNSTransaction(this, res, Transaction::InstallRole); 0437 } 0438 0439 Transaction *KNSBackend::installApplication(AbstractResource *app, const AddonList & /*addons*/) 0440 { 0441 return installApplication(app); 0442 } 0443 0444 int KNSBackend::updatesCount() const 0445 { 0446 return m_updater->updatesCount(); 0447 } 0448 0449 AbstractReviewsBackend *KNSBackend::reviewsBackend() const 0450 { 0451 return m_reviews; 0452 } 0453 0454 static ResultsStream *voidStream() 0455 { 0456 return new ResultsStream(QStringLiteral("KNS-void"), {}); 0457 } 0458 0459 ResultsStream *KNSBackend::search(const AbstractResourcesBackend::Filters &filter) 0460 { 0461 if (!m_isValid || (!filter.resourceUrl.isEmpty() && filter.resourceUrl.scheme() != QLatin1String("kns")) || !filter.mimetype.isEmpty()) 0462 return voidStream(); 0463 0464 if (filter.resourceUrl.scheme() == QLatin1String("kns")) { 0465 return findResourceByPackageName(filter.resourceUrl); 0466 } else if (filter.state >= AbstractResource::Installed) { 0467 auto stream = new KNSResultsStream(this, QStringLiteral("KNS-installed")); 0468 const auto start = [this, stream, filter]() { 0469 if (m_isValid) { 0470 const auto knsFilter = filter.state == AbstractResource::Installed ? KNSCore::Provider::Installed : KNSCore::Provider::Updates; 0471 stream->setRequest(KNSCore::Provider::SearchRequest(KNSCore::Provider::Newest, knsFilter, {}, {}, -1, ENGINE_PAGE_SIZE)); 0472 } 0473 stream->finish(); 0474 }; 0475 if (isFetching()) { 0476 connect(this, &KNSBackend::initialized, stream, start); 0477 } else { 0478 QTimer::singleShot(0, stream, start); 0479 } 0480 0481 return stream; 0482 } else if ((!filter.category && !filter.search.isEmpty()) // Accept global searches 0483 // If there /is/ a category, make sure we actually are one of those requested before searching 0484 || (filter.category && kContains(m_categories, [&filter](const QString &cat) { 0485 return filter.category->matchesCategoryName(cat); 0486 }))) { 0487 return searchStream(filter.search); 0488 } 0489 return voidStream(); 0490 } 0491 0492 KNSResultsStream *KNSBackend::searchStream(const QString &searchText) 0493 { 0494 auto stream = new KNSResultsStream(this, QLatin1String("KNS-search-") + name()); 0495 auto start = [this, stream, searchText]() { 0496 Q_ASSERT(!isFetching()); 0497 if (!m_isValid) { 0498 qWarning() << "querying an invalid backend"; 0499 stream->finish(); 0500 return; 0501 } 0502 KNSCore::Provider::SearchRequest p(KNSCore::Provider::Newest, KNSCore::Provider::None, searchText, {}, 0, ENGINE_PAGE_SIZE); 0503 stream->setRequest(p); 0504 }; 0505 if (isFetching()) { 0506 connect(this, &KNSBackend::initialized, stream, start, Qt::QueuedConnection); 0507 connect(this, &KNSBackend::fetchingChanged, stream, start, Qt::QueuedConnection); 0508 } else { 0509 QTimer::singleShot(0, stream, start); 0510 } 0511 return stream; 0512 } 0513 0514 ResultsStream *KNSBackend::findResourceByPackageName(const QUrl &search) 0515 { 0516 if (search.scheme() != QLatin1String("kns") || search.host() != name()) 0517 return voidStream(); 0518 0519 const auto pathParts = search.path().split(QLatin1Char('/'), Qt::SkipEmptyParts); 0520 if (pathParts.size() != 2) { 0521 Q_EMIT passiveMessage(i18n("Wrong KNewStuff URI: %1", search.toString())); 0522 return voidStream(); 0523 } 0524 const auto providerid = pathParts.at(0); 0525 const auto entryid = pathParts.at(1); 0526 0527 auto stream = new KNSResultsStream(this, QLatin1String("KNS-byname-") + entryid); 0528 auto start = [entryid, stream, providerid]() { 0529 KNSCore::Provider::SearchRequest query(KNSCore::Provider::Newest, KNSCore::Provider::ExactEntryId, entryid, {}, 0, ENGINE_PAGE_SIZE); 0530 stream->setRequest(query); 0531 }; 0532 if (isFetching()) { 0533 connect(this, &KNSBackend::initialized, stream, start, Qt::QueuedConnection); 0534 connect(this, &KNSBackend::fetchingChanged, stream, start, Qt::QueuedConnection); 0535 } else { 0536 QTimer::singleShot(0, stream, start); 0537 } 0538 return stream; 0539 } 0540 0541 bool KNSBackend::isFetching() const 0542 { 0543 return m_fetching; 0544 } 0545 0546 AbstractBackendUpdater *KNSBackend::backendUpdater() const 0547 { 0548 return m_updater; 0549 } 0550 0551 QString KNSBackend::displayName() const 0552 { 0553 return QStringLiteral("KNewStuff"); 0554 } 0555 0556 void KNSBackend::detailsLoaded(const KNSCore::Entry &entry) 0557 { 0558 auto res = resourceForEntry(entry); 0559 Q_EMIT res->longDescriptionChanged(); 0560 } 0561 0562 #include "KNSBackend.moc"