File: watcher.cpp

package info (click to toggle)
kio-extras 4%3A25.04.2-1
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 31,928 kB
  • sloc: cpp: 28,852; ansic: 3,084; perl: 1,048; xml: 116; sh: 92; python: 28; makefile: 9
file content (216 lines) | stat: -rw-r--r-- 7,504 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
// SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>

#include <KDEDModule>
#include <KDirNotify>
#include <KPluginFactory>

#include <QCoreApplication>
#include <QDBusConnection>
#include <QDateTime>
#include <QDebug>
#include <QProcess>
#include <QTimer>

#include "config.h"
#include <smb-logsettings.h>
#include <smburl.h>

class Notifier : public QObject
{
    Q_OBJECT
public:
    explicit Notifier(const QString &url, QObject *parent)
        : QObject(parent)
        , m_url(url)
    {
    }

    ~Notifier() override
    {
        if (m_proc) {
            m_proc->disconnect(); // no need for a finished signal
            m_proc->terminate();
            m_proc->waitForFinished(1000); // we'll want to proceed to kill fairly quickly
            m_proc->kill();
        }
    }

    // Update last event on this notifier.
    // Notifiers that haven't seen activity may get dropped should we run out of capacity.
    void poke()
    {
        m_lastEntry = QDateTime::currentDateTimeUtc();
    }

    bool operator<(const Notifier &other) const
    {
        return m_lastEntry < other.m_lastEntry;
    }

Q_SIGNALS:
    void finished(const QString &url);

public Q_SLOTS:
    void start()
    {
        ++m_startCounter;
        // libsmbclient isn't properly thread safe and attaching a notification request to a context
        // is fully blocking. So notify is blockig the current thread an we can't start more threads
        // with more contexts to watch multiple directories in-process.
        // To bypass this limitation we'll spawn separated notifier processes for each directory
        // we want to notify on.
        // https://bugzilla.samba.org/show_bug.cgi?id=11413
        m_proc = new QProcess(this);
        m_proc->setProcessChannelMode(QProcess::ForwardedChannels);
        m_proc->setProgram(QStringLiteral(KDE_INSTALL_FULL_LIBEXECDIR_KF "/smbnotifier"));
        m_proc->setArguments({m_url});
        connect(m_proc, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &Notifier::maybeRestart);
        m_proc->start();
    }

private Q_SLOTS:
    void maybeRestart(int code, QProcess::ExitStatus status)
    {
        if (code == 0 || status != QProcess::NormalExit || m_startCounter >= m_startCounterLimit) {
            Q_EMIT finished(m_url);
            return;
        }
        m_proc->deleteLater();
        m_proc = nullptr;
        // Try to restart if it error'd out. Notifying requires authentication, if credentials
        // weren't cached by the time we attempted to register the notifier an error will
        // occur and the child exits !0.
        QTimer::singleShot(10000, this, &Notifier::start);
    }

private:
    static const int m_startCounterLimit = 4;
    int m_startCounter = 0;
    const QString m_url;
    QDateTime m_lastEntry{QDateTime::currentDateTimeUtc()};
    QProcess *m_proc = nullptr;
};

class Watcher : public QObject
{
    Q_OBJECT
public:
    explicit Watcher(QObject *parent = nullptr)
        : QObject(parent)
    {
        connect(&m_interface, &OrgKdeKDirNotifyInterface::enteredDirectory, this, &Watcher::watchDirectory);
        connect(&m_interface, &OrgKdeKDirNotifyInterface::leftDirectory, this, &Watcher::unwatchDirectory);
    }

private Q_SLOTS:
    void watchDirectory(const QString &url)
    {
        if (!isInterestingUrl(url)) {
            return;
        }
        auto existingNotifier = m_watches.value(url, nullptr);
        if (existingNotifier) {
            existingNotifier->poke();
            return;
        }
        while (m_watches.count() >= m_capacity) {
            makeSpace();
        }

        // TODO: we could keep track of all potential urls regardless of active notification.
        //   Then closing some tabs in dolphin could lead to more watches freeing up and
        //   us being able to use the free slots for still active urls.

        auto notifier = new Notifier(url, this);
        connect(notifier, &Notifier::finished, this, &Watcher::unwatchDirectory);
        notifier->start();

        m_watches[url] = notifier;
        qCDebug(KIO_SMB_LOG) << "entered" << url << m_watches;
    }

    void unwatchDirectory(const QString &url)
    {
        if (!m_watches.contains(url)) {
            return;
        }
        auto notifier = m_watches.take(url);
        notifier->deleteLater();
        qCDebug(KIO_SMB_LOG) << "leftDirectory" << url << m_watches;
    }

private:
    inline bool isInterestingUrl(const QString &str)
    {
        SMBUrl url{QUrl(str)};
        switch (url.getType()) {
        case SMBURLTYPE_UNKNOWN:
        case SMBURLTYPE_ENTIRE_NETWORK:
        case SMBURLTYPE_WORKGROUP_OR_SERVER:
            return false;
        case SMBURLTYPE_SHARE_OR_PATH:
            return true;
        }
        qCWarning(KIO_SMB_LOG) << "Unexpected url type" << url.getType() << url;
        Q_UNREACHABLE();
        return false;
    }

    void makeSpace()
    {
        auto oldestIt = m_watches.cbegin();
        for (auto it = m_watches.cbegin(); it != m_watches.cend(); ++it) {
            if (*it.value() < *oldestIt.value()) {
                oldestIt = it;
            }
        }
        unwatchDirectory(oldestIt.key());
        qCDebug(KIO_SMB_LOG) << "made space:" << m_watches;
    }

    // Cap the amount of notifiers we can run. Each notifier weighs about 1MiB in private heap
    // depending on the linked/loaded libraries behind KIO so in the interest of staying lightweight
    // we'll want to put a limit on active notifiers even when the user has a bazillion open
    // tabs in dolphin or something. On top of that there's a shared weight of ~3MiB on a plasma
    // session from the actual shared libraries.
    // Further optimizing the notifier would require moving all KIO and qdbus linkage out of
    // the notifier and have a socket pair with this process. The gains are sub 0.5MiB though
    // so given the added complexity I'll deem it unreasonable for now.
    // The better improvement would be to make smbc actually thread safe so we can get rid of the
    // subprocess overhead entirely (and by extension the private heaps of static library objects).
    static const int m_capacity = 10;
    OrgKdeKDirNotifyInterface m_interface{QString(), QString(), QDBusConnection::sessionBus()};
    QHash<QString, Notifier *> m_watches; // watcher is parent of procs
};

/*
    In the json metadata we set:
    X-KDE-Kded-phase=2
    X-KDE-Kded-autoload=true

    Because we need this module loaded all the time, lazy loading on worker use wouldn't
    be sufficient as the kdirnotify signal is already out by the time the worker
    is initalized so the first opened dir wouldn't be watched then.
    It'd be better if we had a general monitor module that workers can register
    with. The monitor would then listen to kdirnotify and check the schemes
    to decide which watcher to load, and then simply forward the call to the watcher
    in-process. Would also save us from having to connect to dbus in every watcher.
*/
class SMBWatcherModule : public KDEDModule
{
    Q_OBJECT
public:
    explicit SMBWatcherModule(QObject *parent, const QVariantList &args)
        : KDEDModule(parent)
    {
        Q_UNUSED(args);
    }

private:
    Watcher m_watcher;
};

K_PLUGIN_FACTORY_WITH_JSON(SMBWatcherModuleFactory, "kded_smbwatcher.json", registerPlugin<SMBWatcherModule>();)

#include "watcher.moc"