Commit c65aa7ef authored by Harald Sitter's avatar Harald Sitter 🌈
Browse files

smb: refactor smbc discovery

this now too is based on the discovery design used for WSD and DNSSD.
advantage being that we can then de-duplicate ALL discovieries through
a single code path.

since smbc technically is meant to be used in a dirent while(){} way I am
pulling some tricks here to get this nicer balanced via the event loop.
ideally smbc would be thread safe or at least allow for multiple contexts,
but currently both scenarios aren't working due to upstream bugs :(
so instead of actually blocking while looping each loop cycle is posted
through the event loop. this gives the other discoverers a chance to
get their signal events in and not get stuck waiting for smbc to do its
thing.

specifically also dnssd/wsd are started before smbc (if applicable anyway)
so they get a head start speed things up quite a bit as previously
they had to wait for smbc to do a full listing.

the discoverer discovers a new smbcdiscovery which isn't terribly
interesting but allows for a much more readable loop logic. furthermore
the readdirplus2 ifdefs have been shuffled a bit, also with the goal
of easing readability.
parent 968a5848
......@@ -50,6 +50,7 @@ set(kio_smb_PART_SRCS
dnssddiscoverer.cpp
discovery.cpp
transfer.cpp
smbcdiscoverer.cpp
)
ecm_qt_declare_logging_category(kio_smb_PART_SRCS
......
......@@ -85,6 +85,7 @@ using namespace KIO;
class SMBSlave : public QObject, public KIO::SlaveBase
{
Q_OBJECT
friend class SMBCDiscoverer;
private:
class SMBError
......
......@@ -41,12 +41,12 @@
#include <QEventLoop>
#include <QTimer>
#include <QUrlQuery>
#include <grp.h>
#include <pwd.h>
#include "dnssddiscoverer.h"
#include "smbcdiscoverer.h"
#include "wsdiscoverer.h"
#include <config-runtime.h>
......@@ -385,7 +385,6 @@ void SMBSlave::reportWarning(const SMBUrl &url, const int errNum)
void SMBSlave::listDir(const QUrl &kurl)
{
qCDebug(KIO_SMB_LOG) << kurl;
int errNum = 0;
// check (correct) URL
QUrl url = checkURL(kurl);
......@@ -398,234 +397,93 @@ void SMBSlave::listDir(const QUrl &kurl)
m_current_url = kurl;
struct smbc_dirent *dirp = nullptr;
UDSEntry udsentry;
bool dir_is_root = true;
QEventLoop e;
int dirfd = smbc_opendir(m_current_url.toSmbcUrl());
if (dirfd > 0) {
errNum = 0;
} else {
errNum = errno;
}
qCDebug(KIO_SMB_LOG) << "open " << m_current_url.toSmbcUrl()
<< "url-type:" << m_current_url.getType()
<< "dirfd:" << dirfd
<< "errNum:" << errNum;
if (dirfd >= 0) {
#ifdef HAVE_READDIRPLUS2
// readdirplus2 improves performance by giving us a stat without separate call (Samba>=4.12)
while (const struct libsmb_file_info *fileInfo = smbc_readdirplus2(dirfd, &st)) {
const QString name = QString::fromUtf8(fileInfo->name);
if (name == ".") {
continue;
} else if (name == "..") {
dir_is_root = false;
continue;
}
udsentry.fastInsert(KIO::UDSEntry::UDS_NAME, name);
m_current_url.addPath(name);
statToUDSEntry(m_current_url, st, udsentry); // won't produce useful error
listEntry(udsentry);
m_current_url.cdUp();
udsentry.clear();
}
#endif // HAVE_READDIRPLUS2
uint direntCount = 0;
do {
qCDebug(KIO_SMB_LOG) << "smbc_readdir ";
dirp = smbc_readdir(dirfd);
if (dirp == nullptr)
break;
++direntCount;
// Set name
QString udsName;
const QString dirpName = QString::fromUtf8(dirp->name);
// We cannot trust dirp->commentlen has it might be with or without the NUL character
// See KDE bug #111430 and Samba bug #3030
const QString comment = QString::fromUtf8(dirp->comment);
if (dirp->smbc_type == SMBC_SERVER || dirp->smbc_type == SMBC_WORKGROUP) {
udsName = dirpName.toLower();
udsName[0] = dirpName.at(0).toUpper();
if (!comment.isEmpty() && dirp->smbc_type == SMBC_SERVER)
udsName += " (" + comment + ')';
} else
udsName = dirpName;
qCDebug(KIO_SMB_LOG) << "dirp->name " << dirp->name << " " << dirpName << " '" << comment << "'" << " " << dirp->smbc_type;
udsentry.fastInsert(KIO::UDSEntry::UDS_NAME, udsName);
udsentry.fastInsert(KIO::UDSEntry::UDS_COMMENT, QString::fromUtf8(dirp->comment));
// Mark all administrative shares, e.g ADMIN$, as hidden. #197903
if (dirpName.endsWith(QLatin1Char('$'))) {
// qCDebug(KIO_SMB_LOG) << dirpName << "marked as hidden";
udsentry.fastInsert(KIO::UDSEntry::UDS_HIDDEN, 1);
}
UDSEntryList list;
QStringList discoveredNames;
if (udsName == ".") {
// Skip the "." entry
// Mind the way m_current_url is handled in the loop
} else if (udsName == "..") {
dir_is_root = false;
// fprintf(stderr,"----------- hide: -%s-\n",dirp->name);
// do nothing and hide the hidden shares
#if !defined(HAVE_READDIRPLUS2)
} else if (dirp->smbc_type == SMBC_FILE || dirp->smbc_type == SMBC_DIR) {
// Set stat information
m_current_url.addPath(dirpName);
const int statErr = browse_stat_path(m_current_url, udsentry);
if (statErr) {
if (statErr == ENOENT || statErr == ENOTDIR) {
reportWarning(m_current_url, statErr);
}
} else {
// Call base class to list entry
listEntry(udsentry);
}
m_current_url.cdUp();
#endif // HAVE_READDIRPLUS2
} else if (dirp->smbc_type == SMBC_SERVER || dirp->smbc_type == SMBC_FILE_SHARE) {
// Set type
udsentry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
if (dirp->smbc_type == SMBC_SERVER) {
udsentry.fastInsert(KIO::UDSEntry::UDS_ACCESS, (S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH));
// QString workgroup = m_current_url.host().toUpper();
QUrl u("smb://");
u.setHost(dirpName);
// when libsmbclient knows
// u = QString("smb://%1?WORKGROUP=%2").arg(dirpName).arg(workgroup.toUpper());
qCDebug(KIO_SMB_LOG) << "list item " << u;
udsentry.fastInsert(KIO::UDSEntry::UDS_URL, u.url());
udsentry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QString::fromLatin1("application/x-smb-server"));
} else
udsentry.fastInsert(KIO::UDSEntry::UDS_ACCESS, (S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH));
// Call base class to list entry
listEntry(udsentry);
} else if (dirp->smbc_type == SMBC_WORKGROUP) {
// Set type
udsentry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
// Set permissions
udsentry.fastInsert(KIO::UDSEntry::UDS_ACCESS, (S_IRUSR | S_IRGRP | S_IROTH | S_IXUSR | S_IXGRP | S_IXOTH));
udsentry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QString::fromLatin1("application/x-smb-workgroup"));
// QString workgroup = m_current_url.host().toUpper();
QUrl u("smb://");
u.setHost(dirpName);
if (!u.isValid()) {
// In the event that the workgroup contains bad characters, put it in a query instead.
// This is transparently handled by SMBUrl when we get this as input again.
// Also see documentation there.
// https://bugs.kde.org/show_bug.cgi?id=204423
u.setHost(QString());
QUrlQuery q;
q.addQueryItem("kio-workgroup", dirpName);
u.setQuery(q);
}
udsentry.fastInsert(KIO::UDSEntry::UDS_URL, u.url());
// Call base class to list entry
listEntry(udsentry);
} else {
qCDebug(KIO_SMB_LOG) << "SMBC_UNKNOWN :" << dirpName;
// TODO: we don't handle SMBC_IPC_SHARE, SMBC_PRINTER_SHARE
// SMBC_LINK, SMBC_COMMS_SHARE
// SlaveBase::error(ERR_INTERNAL, TEXT_UNSUPPORTED_FILE_TYPE);
// continue;
}
udsentry.clear();
} while (dirp); // checked already in the head
const auto flushEntries = [this, &list]() {
if (list.isEmpty()) {
return;
}
listEntries(list);
list.clear();
};
// clean up
smbc_closedir(dirfd);
}
const auto quitLoop = [&e, &flushEntries]() {
flushEntries();
e.quit();
};
// Run service discovery if the path is root. This augments
// "native" results from libsmbclient.
// Also, should native resolution have encountered an error it will not matter.
auto normalizedUrl = url.adjusted(QUrl::NormalizePathSegments);
if (normalizedUrl.path().isEmpty()) {
qCDebug(KIO_SMB_LOG) << "Trying modern discovery (dnssd/wsdiscovery)";
// Since slavebase has no eventloop it wont publish results
// on a timer, since we do not know how long our discovery
// will take this is super meh because we may appear
// stuck for a while. Implement our own listing system
// based on QTimer to mitigate.
QTimer sendTimer;
sendTimer.setInterval(300);
connect(&sendTimer, &QTimer::timeout, this, flushEntries);
sendTimer.start();
QEventLoop e;
QSharedPointer<SMBCDiscoverer> smbc(new SMBCDiscoverer(m_current_url, &e, this));
UDSEntryList list;
QStringList discoveredNames;
QVector<QSharedPointer<Discoverer>> discoverers;
discoverers << smbc;
const auto flushEntries = [this, &list]() {
if (list.isEmpty()) {
return;
}
listEntries(list);
list.clear();
};
auto appendDiscovery = [&](const Discovery::Ptr &discovery) {
if (discoveredNames.contains(discovery->udsName())) {
return;
}
discoveredNames << discovery->udsName();
list.append(discovery->toEntry());
};
const auto quitLoop = [&e, &flushEntries]() {
flushEntries();
e.quit();
};
auto maybeFinished = [&] { // finishes if all discoveries finished
bool allFinished = true;
for (auto discoverer : discoverers) {
allFinished = allFinished && discoverer->isFinished();
}
if (allFinished) {
quitLoop();
}
};
// Since slavebase has no eventloop it wont publish results
// on a timer, since we do not know how long our discovery
// will take this is super meh because we may appear
// stuck for a while. Implement our own listing system
// based on QTimer to mitigate.
QTimer sendTimer;
sendTimer.setInterval(300);
connect(&sendTimer, &QTimer::timeout, this, flushEntries);
sendTimer.start();
connect(smbc.data(), &SMBCDiscoverer::newDiscovery, this, appendDiscovery);
connect(smbc.data(), &SMBCDiscoverer::finished, this, maybeFinished);
DNSSDDiscoverer d;
WSDiscoverer w;
// Run service discovery if the path is root. This augments
// "native" results from libsmbclient.
// Also, should native resolution have encountered an error it will not matter.
if (m_current_url.getType() == SMBURLTYPE_ENTIRE_NETWORK) {
QSharedPointer<DNSSDDiscoverer> dnssd(new DNSSDDiscoverer);
QSharedPointer<WSDiscoverer> wsd(new WSDiscoverer);
discoverers << dnssd << wsd;
const QList<Discoverer *> discoverers {&d, &w};
qCDebug(KIO_SMB_LOG) << "Adding modern discovery (dnssd/wsdiscovery)";
auto appendDiscovery = [&](const Discovery::Ptr &discovery) {
if (discoveredNames.contains(discovery->udsName())) {
return;
}
discoveredNames << discovery->udsName();
list.append(discovery->toEntry());
};
connect(dnssd.data(), &DNSSDDiscoverer::newDiscovery, this, appendDiscovery);
connect(wsd.data(), &WSDiscoverer::newDiscovery, this, appendDiscovery);
auto maybeFinished = [&] { // finishes if all discoveries finished
bool allFinished = true;
for (auto discoverer : discoverers) {
allFinished = allFinished && discoverer->isFinished();
}
if (allFinished) {
quitLoop();
}
};
connect(dnssd.data(), &DNSSDDiscoverer::finished, this, maybeFinished);
connect(wsd.data(), &WSDiscoverer::finished, this, maybeFinished);
connect(&d, &DNSSDDiscoverer::newDiscovery, this, appendDiscovery);
connect(&w, &WSDiscoverer::newDiscovery, this, appendDiscovery);
dnssd->start();
wsd->start();
connect(&d, &DNSSDDiscoverer::finished, this, maybeFinished);
connect(&w, &WSDiscoverer::finished, this, maybeFinished);
qCDebug(KIO_SMB_LOG) << "Modern discovery set up.";
}
d.start();
w.start();
qCDebug(KIO_SMB_LOG) << "Starting discovery.";
smbc->start();
QTimer::singleShot(16000, &e, quitLoop); // max execution time!
e.exec();
QTimer::singleShot(16000, &e, quitLoop); // max execution time!
e.exec();
qCDebug(KIO_SMB_LOG) << "Discovery finished.";
qCDebug(KIO_SMB_LOG) << "Modern discovery finished.";
} else if (dirfd < 0) { // not smb:// and had an error -> handle it
if (errNum == EPERM || errNum == EACCES || workaroundEEXIST(errNum)) {
if (m_current_url.getType() != SMBURLTYPE_ENTIRE_NETWORK && smbc->error() != 0) {
// not smb:// and had an error -> handle it
const int err = smbc->error();
if (err == EPERM || err == EACCES || workaroundEEXIST(err)) {
qCDebug(KIO_SMB_LOG) << "trying checkPassword";
const int passwordError = checkPassword(m_current_url);
if (passwordError == KJob::NoError) {
......@@ -633,7 +491,7 @@ void SMBSlave::listDir(const QUrl &kurl)
finished();
} else if (passwordError == KIO::ERR_USER_CANCELED) {
qCDebug(KIO_SMB_LOG) << "user cancelled password request";
reportError(m_current_url, errNum);
reportError(m_current_url, err);
} else {
qCDebug(KIO_SMB_LOG) << "generic password error:" << passwordError;
error(passwordError, m_current_url.toString());
......@@ -642,12 +500,13 @@ void SMBSlave::listDir(const QUrl &kurl)
return;
}
qCDebug(KIO_SMB_LOG) << "reporting generic error:" << errNum;
reportError(m_current_url, errNum);
qCDebug(KIO_SMB_LOG) << "reporting generic error:" << err;
reportError(m_current_url, err);
return;
}
if (dir_is_root) {
UDSEntry udsentry;
if (smbc->dirWasRoot()) {
udsentry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
udsentry.fastInsert(KIO::UDSEntry::UDS_NAME, ".");
udsentry.fastInsert(KIO::UDSEntry::UDS_ACCESS, (S_IRUSR | S_IRGRP | S_IROTH | S_IXUSR | S_IXGRP | S_IXOTH));
......
/*
SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#include <QCoreApplication>
#include <QUrlQuery>
#include <QScopeGuard>
#include "smbcdiscoverer.h"
static QEvent::Type LoopEvent = QEvent::User;
class SMBCServerDiscovery : public SMBCDiscovery
{
public:
SMBCServerDiscovery(const UDSEntry &entry)
: SMBCDiscovery(entry)
{
m_entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
m_entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, (S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH));
m_entry.fastInsert(KIO::UDSEntry::UDS_URL, url());
m_entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QString::fromLatin1("application/x-smb-server"));
m_entry.fastInsert(KIO::UDSEntry::UDS_ICON_NAME, "network-server");
}
QString url()
{
QUrl u("smb://");
u.setHost(udsName());
return u.url();
}
};
class SMBCShareDiscovery : public SMBCDiscovery
{
public:
SMBCShareDiscovery(const UDSEntry &entry)
: SMBCDiscovery(entry)
{
m_entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
m_entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, (S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH));
}
};
class SMBCWorkgroupDiscovery : public SMBCDiscovery
{
public:
SMBCWorkgroupDiscovery(const UDSEntry &entry)
: SMBCDiscovery(entry)
{
m_entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
m_entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, (S_IRUSR | S_IRGRP | S_IROTH | S_IXUSR | S_IXGRP | S_IXOTH));
m_entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QString::fromLatin1("application/x-smb-workgroup"));
m_entry.fastInsert(KIO::UDSEntry::UDS_URL, url());
}
QString url()
{
QUrl u("smb://");
u.setHost(udsName());
if (!u.isValid()) {
// In the event that the workgroup contains bad characters, put it in a query instead.
// This is transparently handled by SMBUrl when we get this as input again.
// Also see documentation there.
// https://bugs.kde.org/show_bug.cgi?id=204423
u.setHost(QString());
QUrlQuery q;
q.addQueryItem("kio-workgroup", udsName());
u.setQuery(q);
}
return u.url();
}
};
SMBCDiscovery::SMBCDiscovery(const UDSEntry &entry)
: m_entry(entry)
// cache the name, it may get accessed more than once
, m_name(entry.stringValue(KIO::UDSEntry::UDS_NAME))
{
}
QString SMBCDiscovery::udsName() const
{
return m_name;
}
KIO::UDSEntry SMBCDiscovery::toEntry() const
{
return m_entry;
}
SMBCDiscoverer::SMBCDiscoverer(const SMBUrl &url, QEventLoop *loop, SMBSlave *slave)
: m_url(url)
, m_loop(loop)
, m_slave(slave)
{
}
SMBCDiscoverer::~SMBCDiscoverer()
{
if (m_dirFd > 0) {
smbc_closedir(m_dirFd);
}
}
void SMBCDiscoverer::start()
{
queue();
}
bool SMBCDiscoverer::discoverNextFileInfo()
{
#ifdef HAVE_READDIRPLUS2
// Readdirplus2 dir/file listing. Becomes noop when at end of data associated with dirfd.
// If readdirplus2 isn't available the regular dirent listing is done.
// readdirplus2 improves performance by giving us a stat without separate call (Samba>=4.12)
struct stat st;
const struct libsmb_file_info *fileInfo = smbc_readdirplus2(m_dirFd, &st);
if (fileInfo) {
const QString name = QString::fromUtf8(fileInfo->name);
qCDebug(KIO_SMB_LOG) << "fileInfo" << "name:" << name;
if (name == ".") {
return true;
} else if (name == "..") {
m_dirWasRoot = false;
return true;
}
UDSEntry entry;
entry.reserve(5); // Minimal size. stat will set at least 4 fields.
entry.fastInsert(KIO::UDSEntry::UDS_NAME, name);
m_url.addPath(name);
m_slave->statToUDSEntry(m_url, st, entry); // won't produce useful error
emit newDiscovery(Discovery::Ptr(new SMBCDiscovery(entry)));
m_url.cdUp();
return true;
}
#endif // HAVE_READDIRPLUS2
return false;
}
void SMBCDiscoverer::discoverNext()
{
// Poor man's concurrency. smbc isn't thread safe so we'd hold up other
// discoverers until we are done. While that will likely happen anyway
// because smbc_opendir (usually?) blocks until it actually has all
// the data to loop on, meaning the actual looping after open is fairly
// fast. Even so, there's benefit in letting other discoverers do
// their work in the meantime because they may do more atomic
// requests that are async and can take a while due to network latency.
// To get somewhat reasonable behavior we simulate an async smbc discovery
// by posting loop events to the eventloop and each loop run we process
// a single dirent.
// This effectively unblocks the eventloop between iterations.
// Once we are out of entries this discoverer is considered finished.
// Always queue a new iteration when returning so we don't forget to.
auto autoQueue = qScopeGuard([this] {
queue();
});
if (m_dirFd == -1) {
init();
Q_ASSERT(m_dirFd || m_finished);
return;
}
if (discoverNextFileInfo()) {
return;
}
qCDebug(KIO_SMB_LOG) << "smbc_readdir ";
struct smbc_dirent *dirp = smbc_readdir(m_dirFd);
if (dirp == nullptr) {
qCDebug(KIO_SMB_LOG) << "done with smbc";
stop();
return;
}
const QString name = QString::fromUtf8(dirp->name);
// We cannot trust dirp->commentlen has it might be with or without the NUL character
// See KDE bug #111430 and Samba bug #3030
const QString comment = QString::fromUtf8(dirp->comment);
qCDebug(KIO_SMB_LOG) << "dirent "
<< "name:" << name
<< "comment:" << comment
<< "type:" << dirp->smbc_type;
UDSEntry entry;
// Minimal potential size. The actual size depends on this function,
// possibly the stat function, and lastly the Discovery objects themselves.
// The smallest will be a ShareDiscovery with 5 fields.
entry.reserve(5);
entry.fastInsert(KIO::UDSEntry::UDS_NAME, name);
entry.fastInsert(KIO::UDSEntry::UDS_COMMENT, comment);
// Ensure system shares are marked hidden.
if (name.endsWith(QLatin1Char('$'))) {
entry.fastInsert(KIO::UDSEntry::UDS_HIDDEN, 1);
}
#if !defined(HAVE_READDIRPLUS2)
// . and .. are always of the dir type so they are of no consequence outside
// actual dir listing and that'd be done by readdirplus2 already
if (name == ".") {
// Skip the "." entry
// Mind the way m_currentUrl is handled in the loop
} else if (name == "..") {
m_dirWasRoot = false;
} else if (dirp->smbc_type == SMBC_FILE || dirp->smbc_type == SMBC_DIR) {
// Set stat information
m_url.addPath(name);
const int statErr = m_slave->browse_stat_path(m_url, entry);
if (statErr) {
if (statErr == ENOENT || statErr == ENOTDIR) {
m_slave->reportWarning(m_url, statErr);
}
} else {
emit newDiscovery(Discovery::Ptr(new SMBCDiscovery(entry)));
}
m_url.cdUp();
}
#endif // HAVE_READDIRPLUS2
if (dirp->smbc_type == SMBC_SERVER) {
emit newDiscovery(Discovery::Ptr(new SMBCServerDiscovery(entry)));
} else if (dirp->smbc_type == SMBC_FILE_SHARE) {
emit newDiscovery(Discovery::Ptr(new SMBCShareDiscovery(entry)));
} else if (dirp->smbc_type == SMBC_WORKGROUP) {
emit newDiscovery(Discovery::Ptr(new SMBCWorkgroupDiscovery(entry)));
} else {
qCDebug(KIO_SMB_LOG) << "SMBC_UNKNOWN :" << name;
}
}
void SMBCDiscoverer::customEvent(QEvent *event)
{
if (event->type() == LoopEvent) {
if (!m_finished) {