diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-09 09:32:09 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-09 09:32:09 +0200 |
| commit | d213bbffa9f77e78dc21c80308b48b522b0d5a8b (patch) | |
| tree | 0d5e0357fc6b851f45ef5ea130d6aa866ac0f498 /src/mailsync.cpp | |
| parent | 8508ac49d2fe72e362379950b52a64ccd6a6e773 (diff) | |
| download | qtmaildir-d213bbffa9f77e78dc21c80308b48b522b0d5a8b.tar.gz qtmaildir-d213bbffa9f77e78dc21c80308b48b522b0d5a8b.zip | |
fix(sync): clear the pending-edit count on a cron sync
Item 54. A sync fired by the user's cron carries tag edits to the mail
store exactly as a local one does, but only onSyncFinished() cleared the
pending state, so the indicator kept reporting work that had already
shipped and the exit prompt asked to sync for it. Verified against a
real cron run: 31 changes, cleared with no manual sync.
The window cannot see an external run's exit status, and /proc/locks
carries no outcome. It does not need to: mailsync.sh already ends every
run with a "RUN END ... status=OK" banner in its log, which outlives the
process that wrote it. MailSync::lastRunOutcome() reads a bounded tail
of that file and takes the last completed marker, so no change to the
script and no optimistic guessing were needed.
Only a definite OK clears anything. A failed run, a missing or
unreadable log, and a State::Unknown lock reading all leave the count
alone: over-reporting costs a redundant sync, under-reporting costs the
user their edits.
m_editedAccounts is drained in the same place, before flushHeldEdits()
and matching the local path's ordering. Item 49 uses it to choose which
mbsync channels a run syncs, and a count that reached zero while the set
stayed full would look correct and still sync the wrong channels.
The log path comes from a new optional [sync] log key, defaulting to
where the script writes, so a test never reads the developer's own log.
Two notes on the verification, both recorded in the backlog:
- A timing probe endorsed a tail read that was not happening. The
first version of the huge-log test required the call under 100 ms
and passed with the seek deleted, because reading 10 MB is fast
either way. Replaced with an assertion on content.
- Every fixture was invented and the first batch had the wrong
timestamp format, since the script uses date -Iseconds. The tests
passed anyway, because the parser keys on the prefix and the status
token. One test now builds the banner the way the script does.
The before-flushHeldEdits ordering has no test: without a held lock the
flush is a no-op, so both orderings pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'src/mailsync.cpp')
| -rw-r--r-- | src/mailsync.cpp | 55 |
1 files changed, 55 insertions, 0 deletions
diff --git a/src/mailsync.cpp b/src/mailsync.cpp index 1d2a99f..10e5ef7 100644 --- a/src/mailsync.cpp +++ b/src/mailsync.cpp @@ -19,6 +19,8 @@ #include "mailsync.h" #include <QCoreApplication> +#include <QDir> +#include <QFile> #include <QRegularExpression> namespace { @@ -240,3 +242,56 @@ void MailSync::handleError(QProcess::ProcessError error) emit outputReceived(message); emit finished(false, -1); } + +QString MailSync::defaultLogPath() +{ + // Hardcoded to match assets/mailsync.sh, which builds it the same way from + // $HOME. Deriving it from QStandardPaths::GenericStateLocation would append + // the application name and point at a file the script never writes. + return QDir::homePath() + QStringLiteral("/.local/state/mailsync.log"); +} + +SyncOutcome MailSync::lastRunOutcome(const QString &logPath) +{ + if (logPath.isEmpty()) + return SyncOutcome::Unknown; + + QFile file(logPath); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + return SyncOutcome::Unknown; + + // A run's banner is one short line, so a tail this size holds many of them + // even when a verbose mbsync run sits between two. Reading the whole file + // would be a multi-megabyte read on the UI thread every ten minutes. + constexpr qint64 kTailBytes = 64 * 1024; + const qint64 size = file.size(); + if (size > kTailBytes && !file.seek(size - kTailBytes)) + return SyncOutcome::Unknown; + + const QByteArray tail = file.readAll(); + + // Last marker wins: the log accumulates runs, and an older OK must never + // speak for a newer failure. RUN START lines are skipped rather than + // stopping the scan, since the poll that observes a lock release can land + // after the next run has already announced itself. + const QList<QByteArray> lines = tail.split('\n'); + for (auto it = lines.crbegin(); it != lines.crend(); ++it) { + const QByteArray line = it->trimmed(); + if (!line.startsWith("===== RUN END:")) + continue; + + // Matched as a token, not as a whole line: the failure banner carries + // mbsync= and notmuch= fields after the status. + if (line.contains("status=OK")) + return SyncOutcome::Ok; + if (line.contains("status=FAILED")) + return SyncOutcome::Failed; + + // A marker whose status this does not recognise. The script changed, or + // the line was truncated by the tail boundary; either way nothing was + // observed. + return SyncOutcome::Unknown; + } + + return SyncOutcome::Unknown; +} |
