diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -181,6 +181,12 @@ endif() endif() +# KDNSSD before 5.54 suffers from a race condition in avahi's dbus API and +# ideally should not be used in ways that can deadlock a slave. +if(${KF5DNSSD_FOUND} AND ${KF5DNSSD_VERSION} VERSION_GREATER "5.54") + set(HAVE_KDNSSD_WITH_SIGNAL_RACE_PROTECTION TRUE) +endif() + if(SAMBA_FOUND) add_subdirectory(smb) endif() diff --git a/config-runtime.h.cmake b/config-runtime.h.cmake --- a/config-runtime.h.cmake +++ b/config-runtime.h.cmake @@ -14,3 +14,6 @@ * Using the definition from this project so as to be independent of Plasma. */ #define KDE_VERSION_STRING "@PROJECT_VERSION@" + +/* kdnssd with signal race protection */ +#cmakedefine HAVE_KDNSSD_WITH_SIGNAL_RACE_PROTECTION 1 diff --git a/smb/CMakeLists.txt b/smb/CMakeLists.txt --- a/smb/CMakeLists.txt +++ b/smb/CMakeLists.txt @@ -1,3 +1,6 @@ +add_feature_info("SMB DNS-SD Discovery" HAVE_KDNSSD_WITH_SIGNAL_RACE_PROTECTION + "Discover SMB hosts via DNS-SD/Avahi/Bonjour. KF5DNSSD >= 5.54 is required to support this.") + add_definitions(-DTRANSLATION_DOMAIN=\"kio5_smb\") include(CheckIncludeFile) @@ -22,15 +25,24 @@ add_library(kio_smb MODULE ${kio_smb_PART_SRCS}) - -target_link_libraries(kio_smb KF5::KIOCore KF5::I18n ${SAMBA_LIBRARIES} Qt5::Network) +target_link_libraries(kio_smb + KF5::KIOCore + KF5::I18n + ${SAMBA_LIBRARIES} + Qt5::Network + KF5::DNSSD) else() set(kio_smb_PART_SRCS kio_smb_win.cpp) add_library(kio_smb MODULE ${kio_smb_PART_SRCS}) -target_link_libraries(kio_smb KF5::KIOCore KF5::I18n mpr Qt5::Network) +target_link_libraries(kio_smb + KF5::KIOCore + KF5::I18n + mpr + Qt5::Network + KF5::DNSSD) endif() set_target_properties(kio_smb PROPERTIES OUTPUT_NAME "smb") @@ -43,3 +55,4 @@ install( FILES smb-network.desktop DESTINATION ${DATA_INSTALL_DIR}/konqueror/dirtree/remote ) install( FILES smb.protocol DESTINATION ${SERVICES_INSTALL_DIR} ) install( FILES smb-network.desktop DESTINATION ${DATA_INSTALL_DIR}/remoteview ) + diff --git a/smb/kio_smb.h b/smb/kio_smb.h --- a/smb/kio_smb.h +++ b/smb/kio_smb.h @@ -279,6 +279,7 @@ void smbCopyGet(const QUrl& src, const QUrl& dest, int permissions, KIO::JobFlags flags); void smbCopyPut(const QUrl& src, const QUrl& dest, int permissions, KIO::JobFlags flags); bool workaroundEEXIST(const int errNum) const; + void listDNSSD(UDSEntry &udsentry, const QUrl &url, const uint direntCount); void fileSystemFreeSpace(const QUrl &url); diff --git a/smb/kio_smb_browse.cpp b/smb/kio_smb_browse.cpp --- a/smb/kio_smb_browse.cpp +++ b/smb/kio_smb_browse.cpp @@ -13,6 +13,7 @@ //--------------------------------------------------------------------------- // // Copyright (c) 2000 Caldera Systems, Inc. +// Copyright (c) 2018 Harald Sitter // // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the @@ -30,14 +31,21 @@ // ///////////////////////////////////////////////////////////////////////////// -#include -#include - #include "kio_smb.h" #include "kio_smb_internal.h" + +#include +#include #include #include +#include + +#include +#include + +#include + using namespace KIO; int SMBSlave::cache_stat(const SMBUrl &url, struct stat* st ) @@ -330,12 +338,15 @@ qCDebug(KIO_SMB) << "open " << m_current_url.toSmbcUrl() << " " << m_current_url.getType() << " " << dirfd; if(dirfd >= 0) { + uint direntCount = 0; do { qCDebug(KIO_SMB) << "smbc_readdir "; dirp = smbc_readdir(dirfd); if(dirp == nullptr) break; + ++direntCount; + // Set name QString udsName; const QString dirpName = QString::fromUtf8( dirp->name ); @@ -447,6 +458,8 @@ udsentry.clear(); } while (dirp); // checked already in the head + listDNSSD(udsentry, url, direntCount); + if (dir_is_root) { udsentry.insert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); udsentry.insert(KIO::UDSEntry::UDS_NAME, "."); @@ -490,6 +503,116 @@ finished(); } +void SMBSlave::listDNSSD(UDSEntry &udsentry, const QUrl &url, const uint direntCount) +{ + // Certain versions of KDNSSD suffer from signal races which can easily + // deadlock the slave. +#ifndef HAVE_KDNSSD_WITH_SIGNAL_RACE_PROTECTION + return; +#endif // HAVE_KDNSSD_WITH_SIGNAL_RACE_PROTECTION + + // This entire method act as fallback logic iff SMB discovery is not working + // (for example when using a protocol version that doesn't have discovery). + // As such we can return if entries were discovered or the URL is not '/' + auto normalizedUrl = url.adjusted(QUrl::NormalizePathSegments); + if (direntCount > 0 || !normalizedUrl.path().isEmpty()) { + return; + } + + // Slaves have no event loop, start one for the poll. + // KDNSSD has an internal timeout which may trigger if this takes too long + // so in theory this should not ever be able to get stuck. + // The eventloop runs until the discovery is finished. The finished slot + // will quit it. + QList services; + QEventLoop e; + KDNSSD::ServiceBrowser browser(QStringLiteral("_smb._tcp")); + connect(&browser, &KDNSSD::ServiceBrowser::serviceAdded, + this, [&services](KDNSSD::RemoteService::Ptr service){ + qCDebug(KIO_SMB) << "DNSSD added:" + << service->serviceName() + << service->type() + << service->domain() + << service->hostName() + << service->port(); + // Manual contains check. We need to use the == of the underlying + // objects, not the pointers. The same service may have >1 + // RemoteService* instances representing it, so the == impl of + // RemoteService::Ptr is useless here. + for (const auto &it : services) { + if (*service == *it) { + return; + } + } + // Schedule resolution of hostname. We'll later call resolve + // which will block until the resolution is done. This basically + // gives us a head start. + service->resolveAsync(); + services.append(service); + }); + connect(&browser, &KDNSSD::ServiceBrowser::serviceRemoved, + this, [&services](KDNSSD::RemoteService::Ptr service){ + qCDebug(KIO_SMB) << "DNSSD removed:" + << service->serviceName() + << service->type() + << service->domain() + << service->hostName() + << service->port(); + services.removeAll(service); + }); + connect(&browser, &KDNSSD::ServiceBrowser::finished, + this, [&]() { + browser.disconnect(); // Stop sending anything, we'll exit here. + // Resolution still requires an event loop. So, let the resolutions + // finish and then quit the loop. Services that fail resolution + // get dropped since we won't be able to access them properly. + for (auto it = services.begin(); it != services.end(); ++it) { + auto service = *it; + if (!service->resolve()) { + qCWarning(KIO_SMB) << "Failed to resolve DNSSD service" + << service->serviceName(); + it = services.erase(it); + } + } + e.quit(); + }); + browser.startBrowse(); + e.exec(); + + for (const auto &service : services) { + udsentry.fastInsert(KIO::UDSEntry::UDS_NAME, service->serviceName()); + + udsentry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); + udsentry.fastInsert(KIO::UDSEntry::UDS_ACCESS, (S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH)); + + // TODO: it may be better to resolve the host to an ip address. dnssd + // being able to find a service doesn't mean name resolution is + // properly set up for its domain. So, we may not be able to resolve + // this without help from avahi. OTOH KDNSSD doesn't have API for this + // and from a platform POV we should probably assume that if avahi + // is functional it is also set up as resolution provider. + // Given the plugin design on glibc's libnss however I am not sure + // that assumption will be true all the time. ~sitter, 2018 + QUrl u(QStringLiteral("smb://")); + u.setHost(service->hostName()); + if (service->port() > 0 && service->port() != 445 /* default smb */) { + u.setPort(service->port()); + } + + udsentry.fastInsert(KIO::UDSEntry::UDS_URL, u.url()); + udsentry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, + QStringLiteral("application/x-smb-server")); + + listEntry(udsentry); + udsentry.clear(); + } + + // NOTE: workgroups cannot be properly resolved because libsmbclient + // seems to lack the appropriate API to pull this data out of netbios. + // Netbios is also not working on IPv6 and its replacement (LLMNR) + // doesn't support the concept of workgroups. +} + void SMBSlave::fileSystemFreeSpace(const QUrl& url) { qCDebug(KIO_SMB) << url;