File indexing completed on 2024-05-19 08:50:36

0001 // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0002 // SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
0003 
0004 #include <KDEDModule>
0005 #include <KDirNotify>
0006 #include <KPluginFactory>
0007 
0008 #include <QCoreApplication>
0009 #include <QDBusConnection>
0010 #include <QDateTime>
0011 #include <QDebug>
0012 #include <QProcess>
0013 #include <QTimer>
0014 
0015 #include "config.h"
0016 #include <smb-logsettings.h>
0017 #include <smburl.h>
0018 
0019 class Notifier : public QObject
0020 {
0021     Q_OBJECT
0022 public:
0023     explicit Notifier(const QString &url, QObject *parent)
0024         : QObject(parent)
0025         , m_url(url)
0026     {
0027     }
0028 
0029     ~Notifier() override
0030     {
0031         if (m_proc) {
0032             m_proc->disconnect(); // no need for a finished signal
0033             m_proc->terminate();
0034             m_proc->waitForFinished(1000); // we'll want to proceed to kill fairly quickly
0035             m_proc->kill();
0036         }
0037     }
0038 
0039     // Update last event on this notifier.
0040     // Notifiers that haven't seen activity may get dropped should we run out of capacity.
0041     void poke()
0042     {
0043         m_lastEntry = QDateTime::currentDateTimeUtc();
0044     }
0045 
0046     bool operator<(const Notifier &other) const
0047     {
0048         return m_lastEntry < other.m_lastEntry;
0049     }
0050 
0051 Q_SIGNALS:
0052     void finished(const QString &url);
0053 
0054 public Q_SLOTS:
0055     void start()
0056     {
0057         ++m_startCounter;
0058         // libsmbclient isn't properly thread safe and attaching a notification request to a context
0059         // is fully blocking. So notify is blockig the current thread an we can't start more threads
0060         // with more contexts to watch multiple directories in-process.
0061         // To bypass this limitation we'll spawn separated notifier processes for each directory
0062         // we want to notify on.
0063         // https://bugzilla.samba.org/show_bug.cgi?id=11413
0064         m_proc = new QProcess(this);
0065         m_proc->setProcessChannelMode(QProcess::ForwardedChannels);
0066         m_proc->setProgram(QStringLiteral(KDE_INSTALL_FULL_LIBEXECDIR_KF "/smbnotifier"));
0067         m_proc->setArguments({m_url});
0068         connect(m_proc, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &Notifier::maybeRestart);
0069         m_proc->start();
0070     }
0071 
0072 private Q_SLOTS:
0073     void maybeRestart(int code, QProcess::ExitStatus status)
0074     {
0075         if (code == 0 || status != QProcess::NormalExit || m_startCounter >= m_startCounterLimit) {
0076             Q_EMIT finished(m_url);
0077             return;
0078         }
0079         m_proc->deleteLater();
0080         m_proc = nullptr;
0081         // Try to restart if it error'd out. Notifying requires authentication, if credentials
0082         // weren't cached by the time we attempted to register the notifier an error will
0083         // occur and the child exits !0.
0084         QTimer::singleShot(10000, this, &Notifier::start);
0085     }
0086 
0087 private:
0088     static const int m_startCounterLimit = 4;
0089     int m_startCounter = 0;
0090     const QString m_url;
0091     QDateTime m_lastEntry{QDateTime::currentDateTimeUtc()};
0092     QProcess *m_proc = nullptr;
0093 };
0094 
0095 class Watcher : public QObject
0096 {
0097     Q_OBJECT
0098 public:
0099     explicit Watcher(QObject *parent = nullptr)
0100         : QObject(parent)
0101     {
0102         connect(&m_interface, &OrgKdeKDirNotifyInterface::enteredDirectory, this, &Watcher::watchDirectory);
0103         connect(&m_interface, &OrgKdeKDirNotifyInterface::leftDirectory, this, &Watcher::unwatchDirectory);
0104     }
0105 
0106 private Q_SLOTS:
0107     void watchDirectory(const QString &url)
0108     {
0109         if (!isInterestingUrl(url)) {
0110             return;
0111         }
0112         auto existingNotifier = m_watches.value(url, nullptr);
0113         if (existingNotifier) {
0114             existingNotifier->poke();
0115             return;
0116         }
0117         while (m_watches.count() >= m_capacity) {
0118             makeSpace();
0119         }
0120 
0121         // TODO: we could keep track of all potential urls regardless of active notification.
0122         //   Then closing some tabs in dolphin could lead to more watches freeing up and
0123         //   us being able to use the free slots for still active urls.
0124 
0125         auto notifier = new Notifier(url, this);
0126         connect(notifier, &Notifier::finished, this, &Watcher::unwatchDirectory);
0127         notifier->start();
0128 
0129         m_watches[url] = notifier;
0130         qCDebug(KIO_SMB_LOG) << "entered" << url << m_watches;
0131     }
0132 
0133     void unwatchDirectory(const QString &url)
0134     {
0135         if (!m_watches.contains(url)) {
0136             return;
0137         }
0138         auto notifier = m_watches.take(url);
0139         notifier->deleteLater();
0140         qCDebug(KIO_SMB_LOG) << "leftDirectory" << url << m_watches;
0141     }
0142 
0143 private:
0144     inline bool isInterestingUrl(const QString &str)
0145     {
0146         SMBUrl url{QUrl(str)};
0147         switch (url.getType()) {
0148         case SMBURLTYPE_UNKNOWN:
0149         case SMBURLTYPE_ENTIRE_NETWORK:
0150         case SMBURLTYPE_WORKGROUP_OR_SERVER:
0151             return false;
0152         case SMBURLTYPE_SHARE_OR_PATH:
0153             return true;
0154         }
0155         qCWarning(KIO_SMB_LOG) << "Unexpected url type" << url.getType() << url;
0156         Q_UNREACHABLE();
0157         return false;
0158     }
0159 
0160     void makeSpace()
0161     {
0162         auto oldestIt = m_watches.cbegin();
0163         for (auto it = m_watches.cbegin(); it != m_watches.cend(); ++it) {
0164             if (*it.value() < *oldestIt.value()) {
0165                 oldestIt = it;
0166             }
0167         }
0168         unwatchDirectory(oldestIt.key());
0169         qCDebug(KIO_SMB_LOG) << "made space:" << m_watches;
0170     }
0171 
0172     // Cap the amount of notifiers we can run. Each notifier weighs about 1MiB in private heap
0173     // depending on the linked/loaded libraries behind KIO so in the interest of staying lightweight
0174     // we'll want to put a limit on active notifiers even when the user has a bazillion open
0175     // tabs in dolphin or something. On top of that there's a shared weight of ~3MiB on a plasma
0176     // session from the actual shared libraries.
0177     // Further optimizing the notifier would require moving all KIO and qdbus linkage out of
0178     // the notifier and have a socket pair with this process. The gains are sub 0.5MiB though
0179     // so given the added complexity I'll deem it unreasonable for now.
0180     // The better improvement would be to make smbc actually thread safe so we can get rid of the
0181     // subprocess overhead entirely (and by extension the private heaps of static library objects).
0182     static const int m_capacity = 10;
0183     OrgKdeKDirNotifyInterface m_interface{QString(), QString(), QDBusConnection::sessionBus()};
0184     QHash<QString, Notifier *> m_watches; // watcher is parent of procs
0185 };
0186 
0187 /*
0188     In the json metadata we set:
0189     X-KDE-Kded-phase=2
0190     X-KDE-Kded-autoload=true
0191 
0192     Because we need this module loaded all the time, lazy loading on worker use wouldn't
0193     be sufficient as the kdirnotify signal is already out by the time the worker
0194     is initalized so the first opened dir wouldn't be watched then.
0195     It'd be better if we had a general monitor module that workers can register
0196     with. The monitor would then listen to kdirnotify and check the schemes
0197     to decide which watcher to load, and then simply forward the call to the watcher
0198     in-process. Would also save us from having to connect to dbus in every watcher.
0199 */
0200 class SMBWatcherModule : public KDEDModule
0201 {
0202     Q_OBJECT
0203 public:
0204     explicit SMBWatcherModule(QObject *parent, const QVariantList &args)
0205         : KDEDModule(parent)
0206     {
0207         Q_UNUSED(args);
0208     }
0209 
0210 private:
0211     Watcher m_watcher;
0212 };
0213 
0214 K_PLUGIN_FACTORY_WITH_JSON(SMBWatcherModuleFactory, "kded_smbwatcher.json", registerPlugin<SMBWatcherModule>();)
0215 
0216 #include "watcher.moc"