summaryrefslogtreecommitdiffstats
path: root/src/syncmonitor.cpp
blob: e2883dc832353a9ce64038d3d248ef4399486a0e (plain)
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
/*
 * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs
 * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include "syncmonitor.h"

#include <QFile>
#include <QFileInfo>

#include <sys/stat.h>

namespace {

/// Two seconds. The user chose continuous polling over a poll-only-while-
/// quitting variant, and this is a read of one small procfs file, so the cost
/// is negligible next to noticing a cron sync within a couple of seconds.
constexpr int kDefaultIntervalMs = 2000;

}   // namespace

SyncMonitor::SyncMonitor(const QString &lockPath, const QString &locksPath,
                         QObject *parent)
    : QObject(parent), m_lockPath(lockPath), m_locksPath(locksPath)
{
    m_timer.setInterval(kDefaultIntervalMs);
    connect(&m_timer, &QTimer::timeout, this, &SyncMonitor::poll);
}

void SyncMonitor::setInterval(int ms)
{
    m_timer.setInterval(ms);
}

void SyncMonitor::start()
{
    // Poll once immediately: a window opened during a cron sync should say so
    // at once rather than after the first interval.
    poll();
    m_timer.start();
}

void SyncMonitor::stop()
{
    m_timer.stop();
}

QString SyncMonitor::defaultLockPath()
{
    // Must stay equal to LOCKFILE in assets/mailsync.sh.
    return QStringLiteral("/tmp/mbsync.lock");
}

qint64 SyncMonitor::inodeOf(const QString &path)
{
    // Qt exposes no inode accessor, and /proc/locks identifies a file only by
    // device and inode, so this has to come from stat(2) directly. That is also
    // why the whole class is Linux-shaped; see the Unknown state for what
    // happens where /proc/locks does not exist.
    struct stat st;
    if (::stat(QFile::encodeName(path).constData(), &st) != 0)
        return -1;

    return static_cast<qint64>(st.st_ino);
}

bool SyncMonitor::lockHeldIn(const QString &content, qint64 inode)
{
    if (content.isEmpty() || inode < 0)
        return false;

    // A /proc/locks line looks like:
    //   82: FLOCK  ADVISORY  WRITE 9051 fc:00:12058676 0 EOF
    // The inode is the last colon-separated part of the major:minor:inode
    // field. Matching the raw number anywhere in the line would also match a
    // pid or a byte range, so the field is located first and then split.
    const QList<QStringView> lines = QStringView(content).split(u'\n',
                                                               Qt::SkipEmptyParts);
    for (const QStringView &line : lines) {
        const QList<QStringView> fields =
            line.split(u' ', Qt::SkipEmptyParts);

        // Shortest real line still has: index, type, ADVISORY, WRITE, pid,
        // dev:inode. Anything shorter is truncated or not a lock line, and is
        // skipped rather than guessed at.
        if (fields.size() < 6)
            continue;

        // flock(2) only. mailsync.sh uses flock, and a POSIX record lock on
        // the same file belongs to somebody else: the two namespaces cannot
        // see each other, so treating a POSIX entry as ours would report a
        // sync that is not running.
        if (fields.at(1) != QLatin1String("FLOCK"))
            continue;

        for (const QStringView &field : fields) {
            const qsizetype lastColon = field.lastIndexOf(u':');
            if (lastColon < 0)
                continue;

            bool ok = false;
            const qint64 candidate =
                field.mid(lastColon + 1).toLongLong(&ok);
            if (ok && candidate == inode)
                return true;
        }
    }

    return false;
}

void SyncMonitor::poll()
{
    State next = State::Unknown;

    QFile locks(m_locksPath);
    if (locks.open(QIODevice::ReadOnly | QIODevice::Text)) {
        // Read in full rather than line by line: /proc/locks is small, and a
        // partial read while the kernel is editing the table could truncate a
        // line mid-field.
        const QString content = QString::fromUtf8(locks.readAll());

        const qint64 inode = inodeOf(m_lockPath);
        if (inode < 0) {
            // No lock file yet, before the first sync ever runs. The table was
            // readable, so this is a real answer and not Unknown.
            next = State::Idle;
        } else {
            next = lockHeldIn(content, inode) ? State::Running : State::Idle;
        }
    }

    if (next == m_state)
        return;

    m_state = next;
    emit stateChanged(m_state);
}