service/ConnectionHealthMonitor.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "ConnectionHealthMonitor.h" | ||
| 2 | |||
| 3 | #include <QLoggingCategory> | ||
| 4 | #include <QNetworkInformation> | ||
| 5 | #include <QDateTime> | ||
| 6 | #include <QRandomGenerator> | ||
| 7 | |||
| 8 | #include "service/ImapService.h" | ||
| 9 | #include "service/NetworkLifecycleCoordinator.h" | ||
| 10 | |||
| 11 | #ifdef QT_GUI_LIB | ||
| 12 | #include <QGuiApplication> | ||
| 13 | #endif | ||
| 14 | |||
| 15 |
4/7✓ Branch 2 → 3 taken 38 times.
✓ Branch 2 → 8 taken 1218 times.
✓ Branch 4 → 5 taken 38 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 38 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
|
1256 | Q_LOGGING_CATEGORY(lcHealth, "mailjd.health") |
| 16 | |||
| 17 | 971 | ConnectionHealthMonitor::ConnectionHealthMonitor(bool systemWatchEnabled, | |
| 18 | 971 | QObject *parent) | |
| 19 |
3/3✓ Branch 3 → 4 taken 971 times.
✓ Branch 4 → 5 taken 971 times.
✓ Branch 5 → 6 taken 971 times.
|
971 | : QObject(parent), m_systemWatchEnabled(systemWatchEnabled) { |
| 20 |
1/1✓ Branch 7 → 8 taken 971 times.
|
971 | m_probeTimer.setSingleShot(true); |
| 21 | // S92.5.2/S92.5.3: the primary (main) connection carries push and is | ||
| 22 | // probed often; secondaries are checked before use (probeBeforeUse). | ||
| 23 |
3/3✓ Branch 8 → 9 taken 881 times.
✓ Branch 8 → 10 taken 90 times.
✓ Branch 11 → 12 taken 971 times.
|
971 | m_probeTimer.setInterval(m_systemWatchEnabled ? PROBE_PRIMARY_MS : PROBE_SECONDARY_MS); |
| 24 | 987 | m_jitter = [](int minMs, int maxMs) { | |
| 25 | 16 | return int(QRandomGenerator::global()->bounded(minMs, maxMs + 1)); | |
| 26 | 971 | }; | |
| 27 | 971 | connect(&m_probeTimer, &QTimer::timeout, this, | |
| 28 |
1/1✓ Branch 13 → 14 taken 971 times.
|
971 | &ConnectionHealthMonitor::onProbeTimer); |
| 29 | |||
| 30 |
1/1✓ Branch 15 → 16 taken 971 times.
|
971 | m_reconnectTimer.setSingleShot(true); |
| 31 | 971 | connect(&m_reconnectTimer, &QTimer::timeout, this, | |
| 32 |
1/1✓ Branch 16 → 17 taken 971 times.
|
971 | &ConnectionHealthMonitor::onReconnect); |
| 33 | |||
| 34 |
1/1✓ Branch 18 → 19 taken 971 times.
|
971 | m_lifecycle = NetworkLifecycleCoordinator::instance(); |
| 35 | 971 | connect(m_lifecycle, &NetworkLifecycleCoordinator::suspendingChanged, this, | |
| 36 |
1/1✓ Branch 19 → 20 taken 971 times.
|
971 | [this](bool suspending) { |
| 37 |
2/2✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 1 time.
|
2 | if (suspending) |
| 38 | 1 | stopAllTimers(); | |
| 39 | 2 | }); | |
| 40 | 971 | connect(m_lifecycle, &NetworkLifecycleCoordinator::recoveryRequested, this, | |
| 41 |
1/1✓ Branch 21 → 22 taken 971 times.
|
971 | [this](quint64 generation, |
| 42 | NetworkLifecycleCoordinator::Trigger trigger) { | ||
| 43 | 24 | handleRecoveryGeneration(generation, static_cast<int>(trigger)); | |
| 44 | 24 | }); | |
| 45 | |||
| 46 | // System-wide hooks (suspend + network change) are owned by a single | ||
| 47 | // "primary" monitor only — otherwise one OS event would fan out into N | ||
| 48 | // simultaneous forceReconnect() calls (one per secondary connection). | ||
| 49 |
2/2✓ Branch 23 → 24 taken 90 times.
✓ Branch 23 → 35 taken 881 times.
|
971 | if (!m_systemWatchEnabled) { |
| 50 |
5/5✓ Branch 24 → 25 taken 90 times.
✓ Branch 28 → 29 taken 90 times.
✓ Branch 29 → 30 taken 90 times.
✓ Branch 33 → 26 taken 90 times.
✓ Branch 33 → 34 taken 90 times.
|
180 | qCInfo(lcHealth) << "Secondary monitor — system-watch hooks disabled"; |
| 51 | 90 | return; | |
| 52 | } | ||
| 53 | |||
| 54 | // T-72.7: Suspend/resume hook. applicationStateChanged is emitted by | ||
| 55 | // the platform plugin (Windows, macOS, mobile). On Linux desktops it is | ||
| 56 | // unreliable — Inactive simply means "window lost focus", not suspend — | ||
| 57 | // so we do not wire it there at all. The timer-skew fallback in | ||
| 58 | // onProbeTimer() is the resume detector on Linux. | ||
| 59 | #if defined(QT_GUI_LIB) && !defined(Q_OS_LINUX) | ||
| 60 | if (auto *app = qobject_cast<QGuiApplication *>(QCoreApplication::instance())) { | ||
| 61 | connect(app, &QGuiApplication::applicationStateChanged, this, | ||
| 62 | [this](Qt::ApplicationState s) { handleApplicationState(s); }); | ||
| 63 | m_lastAppState = app->applicationState(); | ||
| 64 | } | ||
| 65 | #endif | ||
| 66 | |||
| 67 |
1/1✓ Branch 35 → 36 taken 881 times.
|
881 | setupNetworkHooks(); |
| 68 | ✗ | } | |
| 69 | |||
| 70 | 1904 | ConnectionHealthMonitor::~ConnectionHealthMonitor() { | |
| 71 | 971 | stopAllTimers(); | |
| 72 | 971 | detach(); | |
| 73 | 1904 | } | |
| 74 | |||
| 75 | 881 | void ConnectionHealthMonitor::setupNetworkHooks() { | |
| 76 | // T-72.7: QNetworkInformation is part of Qt Network (already linked as | ||
| 77 | // Qt6::Network per the sprint plan). Try the default backend; if none | ||
| 78 | // is available (minimal container), skip gracefully — probes + timer | ||
| 79 | // skew still detect dead sockets. | ||
| 80 |
2/3✓ Branch 2 → 3 taken 881 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 18 taken 881 times.
|
881 | if (QNetworkInformation::availableBackends().isEmpty()) { |
| 81 | ✗ | qCInfo(lcHealth) << "No QNetworkInformation backend — relying on probes" // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; backend result remains measured. | |
| 82 | ✗ | << "and timer-skew fallback for resume detection"; | |
| 83 | ✗ | return; | |
| 84 | } | ||
| 85 |
1/2✗ Branch 19 → 20 not taken.
✓ Branch 19 → 31 taken 881 times.
|
881 | if (!QNetworkInformation::loadDefaultBackend()) { |
| 86 | ✗ | qCWarning(lcHealth) << "QNetworkInformation::loadDefaultBackend() failed"; // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; backend result remains measured. | |
| 87 | ✗ | return; | |
| 88 | } | ||
| 89 | 881 | m_netInfo = QNetworkInformation::instance(); | |
| 90 |
1/2✗ Branch 32 → 33 not taken.
✓ Branch 32 → 44 taken 881 times.
|
881 | if (!m_netInfo) { |
| 91 | ✗ | qCWarning(lcHealth) << "QNetworkInformation::instance() returned null"; // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; instance guard remains measured. | |
| 92 | ✗ | return; | |
| 93 | } | ||
| 94 | 881 | m_lastReachability = m_netInfo->reachability(); | |
| 95 | 881 | m_lastTransportMedium = m_netInfo->transportMedium(); | |
| 96 | 881 | applyProbeCadence(); | |
| 97 | 881 | connect(m_netInfo, &QNetworkInformation::reachabilityChanged, this, | |
| 98 |
1/1✓ Branch 47 → 48 taken 881 times.
|
881 | [this](QNetworkInformation::Reachability r) { |
| 99 | 3 | const auto previous = m_lastReachability; | |
| 100 | 3 | m_lastReachability = r; | |
| 101 |
2/2✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 2 times.
|
3 | if (!m_active) |
| 102 | 1 | return; | |
| 103 | // Only Disconnected/Unknown → reachable is actionable. Some Qt | ||
| 104 | // backends emit initial or duplicate reachable signals shortly | ||
| 105 | // after startup; those must not tear down a healthy IMAP session. | ||
| 106 |
2/2✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 27 taken 1 time.
|
2 | if (isReachabilityReconnectTransition(previous, r)) { |
| 107 | 3 | qCInfo(lcHealth) << "Network reachability:" << previous << "→" // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; state-change decision remains measured. | |
| 108 |
2/2✓ Branch 16 → 17 taken 1 time.
✓ Branch 17 → 18 taken 1 time.
|
2 | << r << "— forcing reconnect"; |
| 109 | 1 | m_lifecycle->requestRecovery( | |
| 110 | NetworkLifecycleCoordinator::Trigger::Reachability); | ||
| 111 | } | ||
| 112 | }); | ||
| 113 | 881 | connect(m_netInfo, &QNetworkInformation::transportMediumChanged, this, | |
| 114 |
1/1✓ Branch 49 → 50 taken 881 times.
|
881 | [this](QNetworkInformation::TransportMedium m) { |
| 115 | 3 | const auto previous = m_lastTransportMedium; | |
| 116 | 3 | m_lastTransportMedium = m; | |
| 117 | // Metered state is an environment fact: it matters even while | ||
| 118 | // this monitor is inactive (T-87.G2 speculation policy). | ||
| 119 | 3 | emit transportMediumMeteredChanged(transportMediumIsMetered(m)); | |
| 120 | 3 | applyProbeCadence(); | |
| 121 |
2/2✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 2 times.
|
3 | if (!m_active) |
| 122 | 1 | return; | |
| 123 | // Wi-Fi ↔ cellular/carrier changes almost always mean a NAT reset. | ||
| 124 | // Ignore Unknown and initial backend-settling signals. | ||
| 125 |
2/2✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 30 taken 1 time.
|
2 | if (isTransportReconnectTransition(previous, m)) { |
| 126 | 3 | qCInfo(lcHealth) << "Transport medium changed (" << previous // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; state-change decision remains measured. | |
| 127 |
3/3✓ Branch 17 → 18 taken 1 time.
✓ Branch 19 → 20 taken 1 time.
✓ Branch 20 → 21 taken 1 time.
|
2 | << "→" << m << ") — forcing reconnect"; |
| 128 | 1 | m_lifecycle->requestRecovery( | |
| 129 | NetworkLifecycleCoordinator::Trigger::NetworkIdentity); | ||
| 130 | } | ||
| 131 | }); | ||
| 132 |
5/5✓ Branch 51 → 52 taken 881 times.
✓ Branch 55 → 56 taken 881 times.
✓ Branch 56 → 57 taken 881 times.
✓ Branch 63 → 53 taken 881 times.
✓ Branch 63 → 64 taken 881 times.
|
1762 | qCInfo(lcHealth) << "Network-change hooks active via" |
| 133 |
2/2✓ Branch 57 → 58 taken 881 times.
✓ Branch 58 → 59 taken 881 times.
|
881 | << m_netInfo->backendName(); |
| 134 | } | ||
| 135 | |||
| 136 | 970 | void ConnectionHealthMonitor::attach(ImapService *imap) { | |
| 137 |
2/2✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 969 times.
|
970 | if (m_imap == imap) |
| 138 | 1 | return; | |
| 139 | 969 | detach(); | |
| 140 | 969 | m_imap = imap; | |
| 141 |
2/2✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 968 times.
|
969 | if (!m_imap) |
| 142 | 1 | return; | |
| 143 | // Wire through a Qt::QueuedConnection-friendly int slot so the monitor | ||
| 144 | // does not depend on the ImapService enum meta-type at connect time. | ||
| 145 | 968 | connect(m_imap, &ImapService::stateChanged, this, | |
| 146 |
1/1✓ Branch 7 → 8 taken 968 times.
|
1214 | [this](ImapService::State s) { onStateChanged(static_cast<int>(s)); }); |
| 147 | 968 | connect(m_imap, &ImapService::errorOccurred, this, | |
| 148 |
1/1✓ Branch 9 → 10 taken 968 times.
|
968 | &ConnectionHealthMonitor::onErrorOccurred); |
| 149 | 968 | connect(m_imap, &ImapService::connectionAttemptStarted, this, | |
| 150 |
1/1✓ Branch 11 → 12 taken 968 times.
|
968 | &ConnectionHealthMonitor::onConnectionAttemptStarted); |
| 151 | 968 | connect(m_imap, &ImapService::intentionalDisconnect, this, | |
| 152 |
1/1✓ Branch 13 → 14 taken 968 times.
|
968 | &ConnectionHealthMonitor::onIntentionalDisconnect); |
| 153 | 968 | connect(m_imap, &ImapService::disconnectReasonChanged, this, | |
| 154 |
1/1✓ Branch 15 → 16 taken 968 times.
|
1936 | [this](ImapService::DisconnectReason reason) { |
| 155 | 911 | onDisconnectReason(static_cast<int>(reason)); | |
| 156 | 911 | }); | |
| 157 | } | ||
| 158 | |||
| 159 | 2878 | void ConnectionHealthMonitor::detach() { | |
| 160 |
2/2✓ Branch 2 → 3 taken 968 times.
✓ Branch 2 → 4 taken 1910 times.
|
2878 | if (m_imap) |
| 161 | 968 | QObject::disconnect(m_imap, nullptr, this, nullptr); | |
| 162 | 2878 | m_imap = nullptr; | |
| 163 | 2878 | stopAllTimers(); | |
| 164 | 2878 | } | |
| 165 | |||
| 166 | 100 | void ConnectionHealthMonitor::setReconnectConfig(const ImapConfig &cfg) { | |
| 167 | 100 | m_config = cfg; | |
| 168 |
1/2✓ Branch 3 → 4 taken 100 times.
✗ Branch 3 → 5 not taken.
|
100 | if (m_lifecycle) |
| 169 | 100 | m_lifecycle->observeEndpoint(cfg.host); | |
| 170 | 100 | } | |
| 171 | |||
| 172 | 1940 | void ConnectionHealthMonitor::setActive(bool active) { | |
| 173 |
2/2✓ Branch 2 → 3 taken 1787 times.
✓ Branch 2 → 4 taken 153 times.
|
1940 | if (m_active == active) |
| 174 | 1787 | return; | |
| 175 | 153 | m_active = active; | |
| 176 |
7/7✓ Branch 4 → 5 taken 153 times.
✓ Branch 8 → 9 taken 153 times.
✓ Branch 9 → 10 taken 96 times.
✓ Branch 9 → 11 taken 57 times.
✓ Branch 12 → 13 taken 153 times.
✓ Branch 16 → 6 taken 153 times.
✓ Branch 16 → 17 taken 153 times.
|
306 | qCInfo(lcHealth) << (active ? "Monitor activated" : "Monitor deactivated"); |
| 177 |
2/2✓ Branch 17 → 18 taken 57 times.
✓ Branch 17 → 20 taken 96 times.
|
153 | if (!active) { |
| 178 | 57 | stopAllTimers(); | |
| 179 | 57 | m_hadOutage = false; | |
| 180 |
2/2✓ Branch 20 → 21 taken 93 times.
✓ Branch 20 → 26 taken 3 times.
|
96 | } else if (m_imap) { |
| 181 | // Explicit activation is a new user-driven connection lifecycle — | ||
| 182 | // S106-010: including the backoff. The counters of an earlier outage | ||
| 183 | // made the first retry of the new lifecycle wait 160–300 s. | ||
| 184 | 93 | m_intentionallyDisconnected = false; | |
| 185 | 93 | m_reconnectAttempts = 0; | |
| 186 | 93 | m_fastReconnectUsed = false; | |
| 187 | 93 | m_fastAttemptPending = false; | |
| 188 | // Re-arm the probe timer if currently in a probeable state. | ||
| 189 | 93 | ImapService::State s = m_imap->state(); | |
| 190 |
4/4✓ Branch 22 → 23 taken 89 times.
✓ Branch 22 → 25 taken 4 times.
✓ Branch 23 → 24 taken 80 times.
✓ Branch 23 → 25 taken 9 times.
|
93 | if (s == ImapService::State::Authenticated || |
| 191 |
2/2✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 26 taken 79 times.
|
80 | s == ImapService::State::Selected || |
| 192 | s == ImapService::State::Idling) { | ||
| 193 | 14 | m_probeTimer.start(); | |
| 194 | } | ||
| 195 | } | ||
| 196 | } | ||
| 197 | |||
| 198 | 40 | void ConnectionHealthMonitor::onConnectionAttemptStarted() { | |
| 199 | 40 | m_intentionallyDisconnected = false; | |
| 200 | 40 | } | |
| 201 | |||
| 202 | 871 | void ConnectionHealthMonitor::onIntentionalDisconnect() { | |
| 203 | 871 | m_intentionallyDisconnected = true; | |
| 204 | 871 | m_hadOutage = false; | |
| 205 | 871 | stopAllTimers(); | |
| 206 | 871 | } | |
| 207 | |||
| 208 | 911 | void ConnectionHealthMonitor::onDisconnectReason(int reasonInt) { | |
| 209 | 911 | const auto reason = | |
| 210 | static_cast<ImapService::DisconnectReason>(reasonInt); | ||
| 211 | 911 | m_lastDisconnectReason = reasonInt; | |
| 212 | 911 | const bool suppressReconnect = | |
| 213 |
2/2✓ Branch 3 → 4 taken 39 times.
✓ Branch 3 → 7 taken 869 times.
|
908 | reason == ImapService::DisconnectReason::UserRequested || |
| 214 |
2/2✓ Branch 4 → 5 taken 38 times.
✓ Branch 4 → 7 taken 1 time.
|
39 | reason == ImapService::DisconnectReason::AccountReload || |
| 215 |
2/2✓ Branch 5 → 6 taken 37 times.
✓ Branch 5 → 7 taken 1 time.
|
38 | reason == ImapService::DisconnectReason::ApplicationShutdown || |
| 216 |
4/4✓ Branch 2 → 3 taken 908 times.
✓ Branch 2 → 7 taken 3 times.
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 36 times.
|
1819 | reason == ImapService::DisconnectReason::AuthenticationRejected || |
| 217 | reason == ImapService::DisconnectReason::TlsPolicyRejected; | ||
| 218 | 911 | m_intentionallyDisconnected = suppressReconnect; | |
| 219 |
2/2✓ Branch 9 → 10 taken 875 times.
✓ Branch 9 → 11 taken 36 times.
|
911 | if (suppressReconnect) |
| 220 | 875 | stopAllTimers(); | |
| 221 | 911 | } | |
| 222 | |||
| 223 | 1 | void ConnectionHealthMonitor::setProbeInterval(int ms) { | |
| 224 | 1 | m_probeTimer.setInterval(ms); | |
| 225 | 1 | } | |
| 226 | |||
| 227 | 21 | void ConnectionHealthMonitor::forceReconnect(const QString &reason) { | |
| 228 |
2/2✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 16 taken 18 times.
|
21 | if (!m_imap) { |
| 229 | 6 | qCWarning(lcHealth) << "forceReconnect requested but no ImapService" // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; service guard remains measured. | |
| 230 |
2/2✓ Branch 9 → 10 taken 3 times.
✓ Branch 10 → 11 taken 3 times.
|
3 | << "attached — reason:" << reason; |
| 231 | 3 | return; | |
| 232 | } | ||
| 233 |
6/6✓ Branch 16 → 17 taken 18 times.
✓ Branch 20 → 21 taken 18 times.
✓ Branch 21 → 22 taken 18 times.
✓ Branch 22 → 23 taken 18 times.
✓ Branch 26 → 18 taken 18 times.
✓ Branch 26 → 27 taken 18 times.
|
36 | qCInfo(lcHealth) << "forceReconnect:" << reason; |
| 234 | // Reset backoff so resume yields near-immediate recovery rather than | ||
| 235 | // waiting for a probe timeout. Set the outage flag so the next | ||
| 236 | // Authenticated emits connectionRestored(). | ||
| 237 | 18 | m_reconnectAttempts = 0; | |
| 238 | 18 | m_fastReconnectUsed = false; | |
| 239 | 18 | m_fastAttemptPending = false; | |
| 240 | 18 | m_reconnectTimer.stop(); | |
| 241 | 18 | m_hadOutage = true; | |
| 242 |
2/2✓ Branch 28 → 29 taken 17 times.
✓ Branch 28 → 30 taken 1 time.
|
18 | if (m_active) |
| 243 | 17 | m_imap->abortForReconnect(reason); | |
| 244 | } | ||
| 245 | |||
| 246 | 252 | void ConnectionHealthMonitor::onStateChanged(int newStateInt) { | |
| 247 |
2/2✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 251 times.
|
252 | if (!m_imap) |
| 248 | 1 | return; | |
| 249 | 251 | auto newState = static_cast<ImapService::State>(newStateInt); | |
| 250 | |||
| 251 |
3/4✓ Branch 4 → 5 taken 64 times.
✗ Branch 4 → 25 not taken.
✓ Branch 4 → 29 taken 56 times.
✓ Branch 4 → 33 taken 131 times.
|
251 | switch (newState) { |
| 252 | 64 | case ImapService::State::Authenticated: | |
| 253 | case ImapService::State::Selected: | ||
| 254 | // (Re)start the probe timer — these are the states we probe. | ||
| 255 |
2/2✓ Branch 5 → 6 taken 58 times.
✓ Branch 5 → 24 taken 6 times.
|
64 | if (m_active) { |
| 256 | 58 | m_reconnectAttempts = 0; | |
| 257 | 58 | m_fastReconnectUsed = false; | |
| 258 | 58 | m_fastAttemptPending = false; | |
| 259 | 58 | m_lastDisconnectReason = -1; | |
| 260 | 58 | m_reconnectTimer.stop(); | |
| 261 | 58 | m_probeTimer.start(m_probeTimer.interval()); | |
| 262 | 58 | m_lastProbeMsecsSinceEpoch = QDateTime::currentMSecsSinceEpoch(); | |
| 263 |
1/2✓ Branch 10 → 11 taken 58 times.
✗ Branch 10 → 12 not taken.
|
58 | if (m_lifecycle) |
| 264 | 58 | m_lifecycle->markStable(m_lastRecoveryGeneration); | |
| 265 |
2/2✓ Branch 12 → 13 taken 12 times.
✓ Branch 12 → 24 taken 46 times.
|
58 | if (m_hadOutage) { |
| 266 |
5/5✓ Branch 13 → 14 taken 12 times.
✓ Branch 17 → 18 taken 12 times.
✓ Branch 18 → 19 taken 12 times.
✓ Branch 22 → 15 taken 12 times.
✓ Branch 22 → 23 taken 12 times.
|
24 | qCInfo(lcHealth) << "Connection restored after outage"; |
| 267 | 12 | m_hadOutage = false; | |
| 268 | 12 | emit connectionRestored(); | |
| 269 | } | ||
| 270 | } | ||
| 271 | 64 | break; | |
| 272 | ✗ | case ImapService::State::Idling: | |
| 273 | // Still healthy — keep probing (probe will issue IDLE DONE/OK). | ||
| 274 | ✗ | if (m_active) | |
| 275 | ✗ | m_probeTimer.start(m_probeTimer.interval()); | |
| 276 | ✗ | break; | |
| 277 | 56 | case ImapService::State::Error: | |
| 278 | case ImapService::State::Disconnected: | ||
| 279 | 56 | m_probeTimer.stop(); | |
| 280 |
2/2✓ Branch 30 → 31 taken 47 times.
✓ Branch 30 → 32 taken 9 times.
|
56 | if (!m_intentionallyDisconnected) |
| 281 | 47 | scheduleReconnect(); | |
| 282 | 56 | break; | |
| 283 | 131 | default: | |
| 284 | // Connecting/Connected/Greeting/Capability/StartingTLS/Authenticating: | ||
| 285 | // no probe needed, no reconnect scheduled. | ||
| 286 | 131 | break; | |
| 287 | } | ||
| 288 | } | ||
| 289 | |||
| 290 | 31 | void ConnectionHealthMonitor::onErrorOccurred(const QString &error) { | |
| 291 |
6/6✓ Branch 2 → 3 taken 31 times.
✓ Branch 6 → 7 taken 31 times.
✓ Branch 7 → 8 taken 31 times.
✓ Branch 8 → 9 taken 31 times.
✓ Branch 12 → 4 taken 31 times.
✓ Branch 12 → 13 taken 31 times.
|
62 | qCWarning(lcHealth) << "IMAP error:" << error; |
| 292 | // Idempotent — if stateChanged(Error) already armed the timer, this is | ||
| 293 | // a no-op. We DO NOT reset backoff here; only Authenticated does that. | ||
| 294 |
2/2✓ Branch 13 → 14 taken 28 times.
✓ Branch 13 → 15 taken 3 times.
|
31 | if (!m_intentionallyDisconnected) |
| 295 | 28 | scheduleReconnect(); | |
| 296 | 31 | } | |
| 297 | |||
| 298 | 9 | void ConnectionHealthMonitor::onProbeTimer() { | |
| 299 |
3/4✓ Branch 2 → 3 taken 8 times.
✓ Branch 2 → 4 taken 1 time.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 8 times.
|
9 | if (!m_imap || !m_active) |
| 300 | 1 | return; | |
| 301 | |||
| 302 | // T-72.7: timer-skew fallback. applicationStateChanged is unreliable | ||
| 303 | // on Linux desktops; the kernel stops dispatching timers during | ||
| 304 | // suspend, so the probe fires very late when the laptop wakes up. | ||
| 305 | 8 | const auto now = QDateTime::currentMSecsSinceEpoch(); | |
| 306 |
2/2✓ Branch 6 → 7 taken 3 times.
✓ Branch 6 → 26 taken 5 times.
|
8 | if (m_lastProbeMsecsSinceEpoch > 0) { |
| 307 | 3 | const qint64 elapsed = now - m_lastProbeMsecsSinceEpoch; | |
| 308 |
2/2✓ Branch 9 → 10 taken 2 times.
✓ Branch 9 → 26 taken 1 time.
|
3 | if (detectTimerSkew(elapsed, m_probeTimer.interval())) { |
| 309 | 4 | qCInfo(lcHealth) << "Probe fired" << elapsed / 1000 // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; probe threshold remains measured. | |
| 310 |
1/1✓ Branch 17 → 18 taken 2 times.
|
2 | << "s late — treating as resume"; |
| 311 | 2 | m_lastProbeMsecsSinceEpoch = now; | |
| 312 | // S106-009: keep probing. The coordinator may coalesce this request | ||
| 313 | // into a recovery window that ends without a reconnect for this | ||
| 314 | // monitor; without a re-armed timer the connection then had neither a | ||
| 315 | // probe nor a reconnect until some external trigger. | ||
| 316 | 2 | m_probeTimer.start(m_probeTimer.interval()); | |
| 317 | 2 | m_lifecycle->requestRecovery( | |
| 318 | NetworkLifecycleCoordinator::Trigger::TimerSkew); | ||
| 319 | 2 | return; | |
| 320 | } | ||
| 321 | } | ||
| 322 | 6 | m_lastProbeMsecsSinceEpoch = now; | |
| 323 | |||
| 324 | // Ask ImapService for a protocol-aware probe (NOOP or IDLE DONE/OK). | ||
| 325 | // The service's own 15 s watchdog is the second half of the detection | ||
| 326 | // budget; if it fires, failConnection() transitions to Error and our | ||
| 327 | // stateChanged arm schedules the reconnect. | ||
| 328 |
1/1✓ Branch 29 → 30 taken 6 times.
|
12 | m_imap->requestLivenessProbe(QStringLiteral("periodic health check")); |
| 329 | |||
| 330 | // Restart for the next cycle (only meaningful in probeable states). | ||
| 331 | 6 | ImapService::State s = m_imap->state(); | |
| 332 |
4/4✓ Branch 33 → 34 taken 4 times.
✓ Branch 33 → 36 taken 2 times.
✓ Branch 34 → 35 taken 3 times.
✓ Branch 34 → 36 taken 1 time.
|
6 | if (s == ImapService::State::Authenticated || |
| 333 |
2/2✓ Branch 35 → 36 taken 1 time.
✓ Branch 35 → 38 taken 2 times.
|
3 | s == ImapService::State::Selected || |
| 334 | s == ImapService::State::Idling) { | ||
| 335 | 4 | m_probeTimer.start(m_probeTimer.interval()); | |
| 336 | } | ||
| 337 | } | ||
| 338 | |||
| 339 | 5 | void ConnectionHealthMonitor::onReconnect() { | |
| 340 |
3/4✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 4 taken 1 time.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 15 taken 4 times.
|
5 | if (!m_active || !m_imap) { |
| 341 | 2 | qCDebug(lcHealth) << "onReconnect: inactive or no ImapService — skip"; // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; reconnect guard remains measured. | |
| 342 | 1 | return; | |
| 343 | } | ||
| 344 |
2/2✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 28 taken 3 times.
|
4 | if (m_config.host.isEmpty()) { |
| 345 | 2 | qCWarning(lcHealth) << "onReconnect: empty config — skip"; // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; config guard remains measured. | |
| 346 | 1 | return; | |
| 347 | } | ||
| 348 |
6/6✓ Branch 28 → 29 taken 3 times.
✓ Branch 32 → 33 taken 3 times.
✓ Branch 33 → 34 taken 3 times.
✓ Branch 34 → 35 taken 3 times.
✓ Branch 39 → 30 taken 3 times.
✓ Branch 39 → 40 taken 3 times.
|
6 | qCInfo(lcHealth) << "Reconnecting (attempt" << (m_reconnectAttempts + 1) |
| 349 |
1/1✓ Branch 35 → 36 taken 3 times.
|
3 | << ")…"; |
| 350 |
1/1✓ Branch 43 → 44 taken 3 times.
|
3 | emit statusMessage(QStringLiteral("Reconnecting…")); |
| 351 | // ImapService::connectToServer() validates that state is Disconnected | ||
| 352 | // or Error before connecting. abortForReconnect()/failConnection() has | ||
| 353 | // already transitioned us there. | ||
| 354 | 3 | m_imap->connectToServer(m_config); | |
| 355 | // Increment AFTER the attempt so the first delay is the base 5 s, | ||
| 356 | // not 10 s (the off-by-one that SettingsSyncService had). The S92.5.4 | ||
| 357 | // fast attempt is extra: the backoff after it still starts at 5 s. | ||
| 358 |
2/2✓ Branch 47 → 48 taken 2 times.
✓ Branch 47 → 49 taken 1 time.
|
3 | if (m_fastAttemptPending) |
| 359 | 2 | m_fastAttemptPending = false; | |
| 360 | else | ||
| 361 | 1 | ++m_reconnectAttempts; | |
| 362 | } | ||
| 363 | |||
| 364 | 77 | void ConnectionHealthMonitor::scheduleReconnect() { | |
| 365 |
2/2✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 14 taken 76 times.
|
77 | if (m_intentionallyDisconnected) { |
| 366 | 2 | qCDebug(lcHealth) << "scheduleReconnect: intentional disconnect — skip"; // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; disconnect guard remains measured. | |
| 367 | 1 | return; | |
| 368 | } | ||
| 369 |
2/2✓ Branch 14 → 15 taken 2 times.
✓ Branch 14 → 26 taken 74 times.
|
76 | if (!m_active) { |
| 370 |
5/5✓ Branch 15 → 16 taken 2 times.
✓ Branch 19 → 20 taken 2 times.
✓ Branch 20 → 21 taken 2 times.
✓ Branch 24 → 17 taken 2 times.
✓ Branch 24 → 25 taken 2 times.
|
4 | qCDebug(lcHealth) << "scheduleReconnect: inactive — skip"; |
| 371 | 2 | return; | |
| 372 | } | ||
| 373 |
5/6✓ Branch 26 → 27 taken 74 times.
✗ Branch 26 → 30 not taken.
✓ Branch 28 → 29 taken 2 times.
✓ Branch 28 → 30 taken 72 times.
✓ Branch 31 → 32 taken 2 times.
✓ Branch 31 → 43 taken 72 times.
|
74 | if (m_lifecycle && m_lifecycle->isSuspending()) { |
| 374 | // Socket teardown can arrive after PrepareForSleep(true). Preserve the | ||
| 375 | // outage fact, but do not arm a timer that can wake and reconnect while | ||
| 376 | // the process is still in its bounded pre-sleep phase. Native resume will | ||
| 377 | // fan out one recovery generation to every active transport. | ||
| 378 | 2 | m_hadOutage = true; | |
| 379 |
5/5✓ Branch 32 → 33 taken 2 times.
✓ Branch 36 → 37 taken 2 times.
✓ Branch 37 → 38 taken 2 times.
✓ Branch 41 → 34 taken 2 times.
✓ Branch 41 → 42 taken 2 times.
|
4 | qCInfo(lcHealth) << "Deferring reconnect until lifecycle resume"; |
| 380 | 2 | return; | |
| 381 | } | ||
| 382 |
2/2✓ Branch 44 → 45 taken 3 times.
✓ Branch 44 → 56 taken 69 times.
|
72 | if (m_config.host.isEmpty()) { |
| 383 |
5/5✓ Branch 45 → 46 taken 3 times.
✓ Branch 49 → 50 taken 3 times.
✓ Branch 50 → 51 taken 3 times.
✓ Branch 54 → 47 taken 3 times.
✓ Branch 54 → 55 taken 3 times.
|
6 | qCDebug(lcHealth) << "scheduleReconnect: empty config — skip"; |
| 384 | 3 | return; | |
| 385 | } | ||
| 386 | // Idempotent: a second error while the timer is already armed does | ||
| 387 | // NOT shorten or restart the pending reconnect. This avoids the | ||
| 388 | // "staircase" of overlapping single-shots the previous implementations | ||
| 389 | // risked. | ||
| 390 |
2/2✓ Branch 57 → 58 taken 24 times.
✓ Branch 57 → 59 taken 45 times.
|
69 | if (m_reconnectTimer.isActive()) |
| 391 | 24 | return; | |
| 392 | |||
| 393 | 45 | m_hadOutage = true; | |
| 394 |
2/2✓ Branch 60 → 61 taken 25 times.
✓ Branch 60 → 86 taken 20 times.
|
45 | if (fastReconnectAllowed()) { |
| 395 | 25 | const int delayMs = fastReconnectDelayMs(); | |
| 396 | 25 | m_fastReconnectUsed = true; | |
| 397 | 25 | m_fastAttemptPending = true; | |
| 398 |
6/6✓ Branch 62 → 63 taken 25 times.
✓ Branch 66 → 67 taken 25 times.
✓ Branch 67 → 68 taken 25 times.
✓ Branch 68 → 69 taken 25 times.
✓ Branch 78 → 64 taken 25 times.
✓ Branch 78 → 79 taken 25 times.
|
75 | qCInfo(lcHealth) << "Scheduling fast reconnect in" << delayMs |
| 399 |
1/1✓ Branch 69 → 70 taken 25 times.
|
25 | << "ms after transport failure (" |
| 400 |
1/1✓ Branch 71 → 72 taken 25 times.
|
75 | << static_cast<ImapService::DisconnectReason>(m_lastDisconnectReason) |
| 401 |
1/1✓ Branch 72 → 73 taken 25 times.
|
25 | << ")"; |
| 402 | 25 | emit reconnectScheduled((delayMs + 999) / 1000); | |
| 403 | 25 | m_reconnectTimer.start(delayMs); | |
| 404 | // S92.5.6: a silent timeout of the primary means the network path is | ||
| 405 | // dead: one recovery generation lets every other connection of the | ||
| 406 | // process verify itself (this monitor adopts it, its reconnect is armed). | ||
| 407 |
3/4✓ Branch 81 → 82 taken 11 times.
✓ Branch 81 → 85 taken 14 times.
✓ Branch 82 → 83 taken 11 times.
✗ Branch 82 → 85 not taken.
|
25 | if (m_systemWatchEnabled && m_lifecycle && |
| 408 |
2/2✓ Branch 83 → 84 taken 2 times.
✓ Branch 83 → 85 taken 9 times.
|
11 | static_cast<ImapService::DisconnectReason>(m_lastDisconnectReason) == |
| 409 | ImapService::DisconnectReason::CommandTimeout) | ||
| 410 | 2 | m_lifecycle->requestRecovery(NetworkLifecycleCoordinator::Trigger::SocketFailure); | |
| 411 | 25 | return; | |
| 412 | } | ||
| 413 | 20 | const int delay = backoffDelaySeconds(); | |
| 414 |
6/6✓ Branch 87 → 88 taken 20 times.
✓ Branch 91 → 92 taken 20 times.
✓ Branch 92 → 93 taken 20 times.
✓ Branch 93 → 94 taken 20 times.
✓ Branch 100 → 89 taken 20 times.
✓ Branch 100 → 101 taken 20 times.
|
40 | qCInfo(lcHealth) << "Scheduling reconnect in" << delay |
| 415 |
3/3✓ Branch 94 → 95 taken 20 times.
✓ Branch 95 → 96 taken 20 times.
✓ Branch 96 → 97 taken 20 times.
|
20 | << "s (attempt" << (m_reconnectAttempts + 1) << ")"; |
| 416 |
2/2✓ Branch 105 → 106 taken 20 times.
✓ Branch 106 → 107 taken 20 times.
|
40 | emit statusMessage(QStringLiteral("Reconnect in %1s…").arg(delay)); |
| 417 | 20 | emit reconnectScheduled(delay); | |
| 418 | 20 | m_reconnectTimer.start(delay * 1000); | |
| 419 | } | ||
| 420 | |||
| 421 | 2459 | bool ConnectionHealthMonitor::isRecovering() const { | |
| 422 |
6/8✓ Branch 2 → 3 taken 32 times.
✓ Branch 2 → 6 taken 2427 times.
✓ Branch 3 → 4 taken 8 times.
✓ Branch 3 → 6 taken 24 times.
✓ Branch 4 → 5 taken 8 times.
✗ Branch 4 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 8 times.
|
2459 | if (!m_active || !m_hadOutage || m_intentionallyDisconnected || !m_imap) |
| 423 | 2451 | return false; | |
| 424 |
1/2✓ Branch 8 → 9 taken 8 times.
✗ Branch 8 → 10 not taken.
|
8 | if (m_reconnectTimer.isActive()) |
| 425 | 8 | return true; | |
| 426 | ✗ | switch (m_imap->state()) { | |
| 427 | ✗ | case ImapService::State::Connecting: | |
| 428 | case ImapService::State::Connected: | ||
| 429 | case ImapService::State::Greeting: | ||
| 430 | case ImapService::State::Capability: | ||
| 431 | case ImapService::State::StartingTLS: | ||
| 432 | case ImapService::State::Authenticating: | ||
| 433 | ✗ | return true; | |
| 434 | ✗ | default: | |
| 435 | ✗ | return false; | |
| 436 | } | ||
| 437 | } | ||
| 438 | |||
| 439 | // S92.5.4: one fast attempt per outage, only for a failure of the transport | ||
| 440 | // (the server itself did not refuse us) while the network is reachable. | ||
| 441 | 45 | bool ConnectionHealthMonitor::fastReconnectAllowed() const { | |
| 442 |
3/4✓ Branch 2 → 3 taken 42 times.
✓ Branch 2 → 4 taken 3 times.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 42 times.
|
45 | if (m_fastReconnectUsed || m_reconnectAttempts != 0) |
| 443 | 3 | return false; | |
| 444 |
2/2✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 41 times.
|
42 | if (m_lastReachability == QNetworkInformation::Reachability::Disconnected) |
| 445 | 1 | return false; | |
| 446 | using Reason = ImapService::DisconnectReason; | ||
| 447 |
2/2✓ Branch 7 → 8 taken 25 times.
✓ Branch 7 → 9 taken 16 times.
|
41 | switch (static_cast<Reason>(m_lastDisconnectReason)) { |
| 448 | 25 | case Reason::SocketError: | |
| 449 | case Reason::CommandTimeout: | ||
| 450 | case Reason::ForcedHealthProbe: | ||
| 451 | 25 | return true; | |
| 452 | 16 | default: | |
| 453 | 16 | return false; | |
| 454 | } | ||
| 455 | } | ||
| 456 | |||
| 457 | // Primary first, secondaries after it: one outage of an account produces a | ||
| 458 | // staggered sequence of attempts instead of a burst. | ||
| 459 | 25 | int ConnectionHealthMonitor::fastReconnectDelayMs() const { | |
| 460 | 25 | return m_systemWatchEnabled | |
| 461 |
2/2✓ Branch 2 → 3 taken 11 times.
✓ Branch 2 → 5 taken 14 times.
|
25 | ? m_jitter(FAST_RECONNECT_PRIMARY_MIN_MS, FAST_RECONNECT_PRIMARY_MAX_MS) |
| 462 | 25 | : m_jitter(FAST_RECONNECT_SECONDARY_MIN_MS, FAST_RECONNECT_SECONDARY_MAX_MS); | |
| 463 | } | ||
| 464 | |||
| 465 | 32 | int ConnectionHealthMonitor::backoffDelaySeconds() const { | |
| 466 | // Corrected from SettingsSyncService: NO pre-increment, so the first | ||
| 467 | // delay is the base 5 s. delay = 5 * 2^min(attemptIndex, 20) capped at | ||
| 468 | // 300 s → 5, 10, 20, 40, 80, 160, 300, 300, … | ||
| 469 | // Bug-24/T-405: cap the shift to 20 to prevent integer overflow UB | ||
| 470 | // when m_reconnectAttempts >= 31. | ||
| 471 | 32 | const int capped = std::min(m_reconnectAttempts, 20); | |
| 472 | 32 | return std::min(5 * (1 << capped), MAX_RECONNECT_DELAY); | |
| 473 | } | ||
| 474 | |||
| 475 | 5653 | void ConnectionHealthMonitor::stopAllTimers() { | |
| 476 | 5653 | m_probeTimer.stop(); | |
| 477 | 5653 | m_reconnectTimer.stop(); | |
| 478 | 5653 | } | |
| 479 | |||
| 480 | 5 | void ConnectionHealthMonitor::handleApplicationState( | |
| 481 | Qt::ApplicationState current) { | ||
| 482 |
2/2✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 4 times.
|
5 | if (!m_active) |
| 483 | 1 | return; | |
| 484 | // Suspended → Active = "the system just woke up". A network roaming | ||
| 485 | // event almost certainly accompanied the resume, so force a reconnect | ||
| 486 | // instead of waiting for a probe timeout. | ||
| 487 | // | ||
| 488 | // Sprint 72 post-review fix: ApplicationInactive must NOT be treated as | ||
| 489 | // "suspended". On Linux desktops (XCB/Wayland) and even on Windows/macOS | ||
| 490 | // when another window takes focus, Inactive simply means "not the active | ||
| 491 | // window". Treating it as resume tore down healthy IMAP connections on | ||
| 492 | // every focus change. Only the genuine Suspended state (mobile | ||
| 493 | // backgrounding / system sleep) is actionable. | ||
| 494 | 4 | const bool wasSuspended = m_lastAppState == Qt::ApplicationSuspended; | |
| 495 | 4 | const bool nowActive = current == Qt::ApplicationActive; | |
| 496 |
3/4✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 27 taken 3 times.
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 27 not taken.
|
4 | if (wasSuspended && nowActive) { |
| 497 |
5/5✓ Branch 6 → 7 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 25 → 8 taken 1 time.
✓ Branch 25 → 26 taken 1 time.
|
3 | qCInfo(lcHealth) << "Application resumed (state" |
| 498 |
3/3✓ Branch 13 → 14 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 16 → 17 taken 1 time.
|
3 | << m_lastAppState << "→" << current |
| 499 |
1/1✓ Branch 17 → 18 taken 1 time.
|
1 | << ") — forcing reconnect"; |
| 500 | 1 | m_lifecycle->requestRecovery( | |
| 501 | NetworkLifecycleCoordinator::Trigger::QtResume); | ||
| 502 | } | ||
| 503 | 4 | m_lastAppState = current; | |
| 504 | } | ||
| 505 | |||
| 506 | 29 | void ConnectionHealthMonitor::handleRecoveryGeneration(quint64 generation, | |
| 507 | int trigger) { | ||
| 508 |
3/4✓ Branch 2 → 3 taken 29 times.
✗ Branch 2 → 4 not taken.
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 28 times.
|
29 | if (generation == 0 || generation <= m_lastRecoveryGeneration) |
| 509 | 1 | return; | |
| 510 | 28 | m_lastRecoveryGeneration = generation; | |
| 511 | 28 | m_lastProbeMsecsSinceEpoch = 0; | |
| 512 |
2/2✓ Branch 5 → 6 taken 3 times.
✓ Branch 5 → 7 taken 25 times.
|
28 | if (!m_active) |
| 513 | 3 | return; | |
| 514 | |||
| 515 | // A long suspend can let the remote peer or an intermediate NAT expire the | ||
| 516 | // socket. When Qt dispatches the queued events after resume, that stale | ||
| 517 | // socket error may reach us just before logind's PrepareForSleep(false). | ||
| 518 | // In that ordering the ordinary error path has already armed (or even | ||
| 519 | // started) the one reconnect this transport needs. Aborting it here would | ||
| 520 | // turn one lifecycle generation into two connection attempts. Adopt that | ||
| 521 | // in-flight recovery into the new generation; the next authenticated state | ||
| 522 | // will mark this generation stable in the normal way. | ||
| 523 |
2/2✓ Branch 7 → 8 taken 23 times.
✓ Branch 7 → 9 taken 2 times.
|
25 | const ImapService::State state = m_imap ? m_imap->state() |
| 524 | 25 | : ImapService::State::Disconnected; | |
| 525 | 25 | const bool connectionAttemptInProgress = | |
| 526 |
1/2✓ Branch 11 → 12 taken 24 times.
✗ Branch 11 → 16 not taken.
|
24 | state == ImapService::State::Connecting || |
| 527 |
1/2✓ Branch 12 → 13 taken 24 times.
✗ Branch 12 → 16 not taken.
|
24 | state == ImapService::State::Connected || |
| 528 |
1/2✓ Branch 13 → 14 taken 24 times.
✗ Branch 13 → 16 not taken.
|
24 | state == ImapService::State::Greeting || |
| 529 |
1/2✓ Branch 14 → 15 taken 24 times.
✗ Branch 14 → 16 not taken.
|
24 | state == ImapService::State::Capability || |
| 530 |
3/4✓ Branch 10 → 11 taken 24 times.
✓ Branch 10 → 16 taken 1 time.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 24 times.
|
49 | state == ImapService::State::StartingTLS || |
| 531 | state == ImapService::State::Authenticating; | ||
| 532 |
4/4✓ Branch 18 → 19 taken 4 times.
✓ Branch 18 → 23 taken 21 times.
✓ Branch 24 → 25 taken 3 times.
✓ Branch 24 → 40 taken 22 times.
|
29 | if (m_hadOutage && |
| 533 |
4/4✓ Branch 20 → 21 taken 2 times.
✓ Branch 20 → 22 taken 2 times.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 23 taken 1 time.
|
4 | (m_reconnectTimer.isActive() || connectionAttemptInProgress)) { |
| 534 |
5/5✓ Branch 25 → 26 taken 3 times.
✓ Branch 29 → 30 taken 3 times.
✓ Branch 30 → 31 taken 3 times.
✓ Branch 38 → 27 taken 3 times.
✓ Branch 38 → 39 taken 3 times.
|
6 | qCInfo(lcHealth) << "Adopting already pending reconnect into lifecycle" |
| 535 |
4/4✓ Branch 31 → 32 taken 3 times.
✓ Branch 32 → 33 taken 3 times.
✓ Branch 33 → 34 taken 3 times.
✓ Branch 34 → 35 taken 3 times.
|
3 | << "generation" << generation << "trigger" << trigger; |
| 536 | 3 | return; | |
| 537 | } | ||
| 538 | // S92.5.6: the primary found the path dead; a connection that still | ||
| 539 | // works must not be torn down, so it verifies itself instead. | ||
| 540 |
2/2✓ Branch 40 → 41 taken 4 times.
✓ Branch 40 → 50 taken 18 times.
|
22 | if (trigger == static_cast<int>(NetworkLifecycleCoordinator::Trigger::SocketFailure)) { |
| 541 |
1/2✓ Branch 41 → 42 taken 4 times.
✗ Branch 41 → 49 not taken.
|
4 | if (m_imap) |
| 542 |
1/1✓ Branch 45 → 46 taken 4 times.
|
8 | m_imap->verifyAfterPathFailure(QStringLiteral("primary probe timeout")); |
| 543 | 4 | return; | |
| 544 | } | ||
| 545 | |||
| 546 |
1/1✓ Branch 57 → 58 taken 18 times.
|
54 | forceReconnect(QStringLiteral("lifecycle generation %1 trigger %2") |
| 547 |
1/1✓ Branch 54 → 55 taken 18 times.
|
36 | .arg(generation) |
| 548 |
1/1✓ Branch 56 → 57 taken 18 times.
|
54 | .arg(trigger)); |
| 549 | } | ||
| 550 | |||
| 551 | ✗ | bool ConnectionHealthMonitor::isTransportMediumMetered() const { | |
| 552 | ✗ | return transportMediumIsMetered(m_lastTransportMedium); | |
| 553 | } | ||
| 554 | |||
| 555 | 3 | bool ConnectionHealthMonitor::transportMediumIsMetered( | |
| 556 | QNetworkInformation::TransportMedium medium) { | ||
| 557 | // Unknown (including no backend) reports metered so speculation defaults | ||
| 558 | // to the conservative mode without platform signals (T-87.G2). | ||
| 559 |
1/2✓ Branch 3 → 4 taken 3 times.
✗ Branch 3 → 5 not taken.
|
3 | return medium == QNetworkInformation::TransportMedium::Cellular || |
| 560 |
2/4✓ Branch 2 → 3 taken 3 times.
✗ Branch 2 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 3 times.
|
6 | medium == QNetworkInformation::TransportMedium::Bluetooth || |
| 561 | 3 | medium == QNetworkInformation::TransportMedium::Unknown; | |
| 562 | } | ||
| 563 | |||
| 564 | // S92.5.3: a known cellular/bluetooth link keeps the conservative 15 s | ||
| 565 | // cadence (radio wake-ups); every other medium, including an unknown one, | ||
| 566 | // uses the primary cadence. The probe itself is a few dozen bytes. | ||
| 567 | 887 | void ConnectionHealthMonitor::applyProbeCadence() { | |
| 568 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 887 times.
|
887 | if (!m_systemWatchEnabled) |
| 569 | ✗ | return; | |
| 570 | 887 | const bool knownMetered = | |
| 571 |
2/2✓ Branch 4 → 5 taken 886 times.
✓ Branch 4 → 6 taken 1 time.
|
1773 | m_lastTransportMedium == QNetworkInformation::TransportMedium::Cellular || |
| 572 |
1/2✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 886 times.
|
886 | m_lastTransportMedium == QNetworkInformation::TransportMedium::Bluetooth; |
| 573 |
2/2✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 886 times.
|
887 | const int interval = knownMetered ? PROBE_METERED_MS : PROBE_PRIMARY_MS; |
| 574 |
2/2✓ Branch 12 → 13 taken 2 times.
✓ Branch 12 → 31 taken 885 times.
|
887 | if (m_probeTimer.interval() != interval) { |
| 575 |
7/7✓ Branch 13 → 14 taken 2 times.
✓ Branch 17 → 18 taken 2 times.
✓ Branch 18 → 19 taken 2 times.
✓ Branch 19 → 20 taken 2 times.
✓ Branch 20 → 21 taken 2 times.
✓ Branch 29 → 15 taken 2 times.
✓ Branch 29 → 30 taken 2 times.
|
6 | qCInfo(lcHealth) << "Probe cadence" << interval << "ms (medium" |
| 576 |
2/2✓ Branch 22 → 23 taken 2 times.
✓ Branch 23 → 24 taken 2 times.
|
4 | << m_lastTransportMedium << ")"; |
| 577 | 2 | m_probeTimer.setInterval(interval); | |
| 578 | } | ||
| 579 | } | ||
| 580 |