Branch data Line data Source code
1 : : #pragma once
2 : :
3 : : #include <QObject>
4 : : #include <QNetworkInformation>
5 : : #include <QString>
6 : : #include <QTimer>
7 : :
8 : : #include "data/AccountConfig.h"
9 : :
10 : : class ImapService;
11 : :
12 : : // ConnectionHealthMonitor (Sprint 72) wraps a single ImapService with three
13 : : // responsibilities that previously lived fragmented across MainWindow,
14 : : // MailController and SettingsSyncService:
15 : : //
16 : : // 1. Periodic liveness probes (15 s) — calls
17 : : // ImapService::requestLivenessProbe(); ImapService decides whether
18 : : // that is a NOOP (Authenticated/Selected) or an IDLE DONE/OK
19 : : // round-trip (Idling) and runs a 15 s watchdog. Combined worst-case
20 : : // dead-socket detection ≈ 30 s (vs. ~40 min before Sprint 72).
21 : : // 2. Exponential-backoff reconnect — 5, 10, 20, 40, 80, 160, 300, 300, …
22 : : // seconds (corrected from SettingsSyncService, whose pre-increment
23 : : // started at 10 s). The Bug-24/T-405 overflow guard is preserved.
24 : : // 3. System suspend/resume + network-change reactivity — on
25 : : // applicationSuspended → applicationActive and on offline → online /
26 : : // transport change it calls forceReconnect() so recovery starts in
27 : : // the first backoff window instead of waiting for a probe timeout.
28 : : //
29 : : // The monitor never touches private ImapService internals; it goes through
30 : : // the two public methods added in T-72.2 (requestLivenessProbe /
31 : : // abortForReconnect). One monitor instance per ImapService.
32 : : class ConnectionHealthMonitor : public QObject {
33 : : Q_OBJECT
34 : : #ifdef MAILJD_UNIT_TEST
35 : : friend class TestConnectionHealth;
36 : : #endif
37 : :
38 : : public:
39 : : // systemWatchEnabled=true (default) marks this monitor as the single
40 : : // "primary" instance that wires the system-wide suspend + network-change
41 : : // hooks. Secondary monitors (body/search/sync) pass false so one OS event
42 : : // does not trigger N simultaneous forceReconnect() calls (Sprint 72
43 : : // post-review fix). Secondary monitors still run their own liveness
44 : : // probes and reconnect on real socket failures / state errors.
45 : : explicit ConnectionHealthMonitor(bool systemWatchEnabled = true,
46 : : QObject *parent = nullptr);
47 : : ~ConnectionHealthMonitor() override;
48 : :
49 : : // Wire the monitor to an ImapService. Safe to call again after detach().
50 : : void attach(ImapService *imap);
51 : : // Disconnect signal/slot wiring from the previously-attached ImapService.
52 : : // Call BEFORE the service is destroyed or replaced so monitor slots
53 : : // cannot react to the dying socket's stateChanged/errorOccurred.
54 : : void detach();
55 : :
56 : : // Account configuration for reconnect attempts. Empty host = invalid;
57 : : // scheduleReconnect() becomes a no-op until a real config is set.
58 : : void setReconnectConfig(const ImapConfig &cfg);
59 : : ImapConfig reconnectConfig() const { return m_config; }
60 : :
61 : : // false during shutdown / account switch — stops both timers and
62 : : // suppresses scheduleReconnect(). True re-enables probing in
63 : : // Authenticated/Selected/Idling.
64 : : void setActive(bool active);
65 : 3 : bool isActive() const { return m_active; }
66 : :
67 : : // Probe interval (default PROBE_DEFAULT_MS). A shorter interval makes
68 : : // dead-socket detection faster at the cost of more NOOP traffic.
69 : : void setProbeInterval(int ms);
70 : :
71 : : // Force immediate teardown + reconnect (suspend, network change).
72 : : // Resets backoff, sets the outage flag, and asks the ImapService to
73 : : // abort — the resulting State::Error fires the monitor's own reconnect
74 : : // arm with the first 5 s delay.
75 : : void forceReconnect(const QString &reason);
76 : :
77 : : // T-72.7 unit-test seams — direct hooks for the suspend/network logic.
78 : : // Inline so tests do not need a QGuiApplication event loop.
79 : : // NOTE: only ApplicationSuspended counts as "was suspended". On Linux
80 : : // desktops ApplicationInactive simply means "window lost focus"; treating
81 : : // it as resume tore down healthy connections on every focus change
82 : : // (Sprint 72 post-review fix).
83 : : void handleApplicationState(Qt::ApplicationState current);
84 : 42 : static bool detectTimerSkew(qint64 elapsedMs, qint64 intervalMs) {
85 : : // Treat only a clear multi-minute gap as resume. A busy UI thread can
86 : : // delay a 15 s timer by 30+ seconds without any suspend/network change.
87 [ + + + - ]: 42 : return elapsedMs >= 5 * 60 * 1000 && elapsedMs >= 10 * intervalMs;
88 : : }
89 : 12 : static bool isReachabilityReconnectTrigger(
90 : : QNetworkInformation::Reachability reachability) {
91 [ + + ]: 10 : return reachability == QNetworkInformation::Reachability::Local ||
92 [ + + + + ]: 22 : reachability == QNetworkInformation::Reachability::Site ||
93 : 12 : reachability == QNetworkInformation::Reachability::Online;
94 : : }
95 : 5 : static bool isReachabilityReconnectTransition(
96 : : QNetworkInformation::Reachability previous,
97 : : QNetworkInformation::Reachability current) {
98 [ + + + - ]: 7 : return !isReachabilityReconnectTrigger(previous) &&
99 : 7 : isReachabilityReconnectTrigger(current);
100 : : }
101 : 4 : static bool isTransportReconnectTransition(
102 : : QNetworkInformation::TransportMedium previous,
103 : : QNetworkInformation::TransportMedium current) {
104 [ + + ]: 3 : return previous != QNetworkInformation::TransportMedium::Unknown &&
105 [ + + + + ]: 7 : current != QNetworkInformation::TransportMedium::Unknown &&
106 : 4 : previous != current;
107 : : }
108 : :
109 : : // Test introspection.
110 : 3 : int reconnectAttempts() const { return m_reconnectAttempts; }
111 : 12 : bool isReconnectScheduled() const { return m_reconnectTimer.isActive(); }
112 : 4 : int reconnectIntervalSeconds() const {
113 [ + - ]: 4 : return m_reconnectTimer.isActive() ? m_reconnectTimer.interval() / 1000
114 : 4 : : -1;
115 : : }
116 : :
117 : : signals:
118 : : void statusMessage(const QString &message);
119 : : void reconnectScheduled(int delaySeconds);
120 : : void connectionRestored();
121 : :
122 : : private slots:
123 : : void onStateChanged(int newState);
124 : : void onErrorOccurred(const QString &error);
125 : : void onProbeTimer();
126 : : void onReconnect();
127 : :
128 : : private:
129 : : void scheduleReconnect();
130 : : int backoffDelaySeconds() const;
131 : : void stopAllTimers();
132 : : void setupNetworkHooks();
133 : :
134 : : ImapService *m_imap = nullptr;
135 : : ImapConfig m_config;
136 : : bool m_active = false;
137 : : // ctor param: only the primary monitor wires the suspend + network hooks.
138 : : bool m_systemWatchEnabled = true;
139 : : bool m_hadOutage = false;
140 : :
141 : : QTimer m_probeTimer;
142 : : QTimer m_reconnectTimer;
143 : : int m_reconnectAttempts = 0;
144 : :
145 : : // T-72.7: suspend/resume detection (QGuiApplication::applicationStateChanged).
146 : : Qt::ApplicationState m_lastAppState = Qt::ApplicationActive;
147 : : // T-72.7: timer-skew fallback for Linux desktops where
148 : : // applicationState is unreliable. Wall-clock at the last probe fire.
149 : : qint64 m_lastProbeMsecsSinceEpoch = 0;
150 : :
151 : : // T-72.7: optional network-change hooks. Null if no backend available
152 : : // (e.g. minimal Linux container) — probes + timer-skew still cover us.
153 : : QNetworkInformation *m_netInfo = nullptr;
154 : : QNetworkInformation::Reachability m_lastReachability =
155 : : QNetworkInformation::Reachability::Unknown;
156 : : QNetworkInformation::TransportMedium m_lastTransportMedium =
157 : : QNetworkInformation::TransportMedium::Unknown;
158 : :
159 : : static constexpr int MAX_RECONNECT_DELAY = 300; // 5 min cap (Bug-24 guard)
160 : : static constexpr int PROBE_DEFAULT_MS = 15000; // 15 s + ImapService 15 s watchdog = ~30 s
161 : : };
|