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"