11import 'dart:async' ;
22import 'dart:collection' ;
3- import 'dart:io' show Platform;
3+ import 'dart:io' show Platform, Process ;
44
55import 'package:flutter/foundation.dart' ;
66import 'package:sentracore_dashboard/models/system_state.dart' ;
@@ -104,6 +104,22 @@ class EngineProvider extends ChangeNotifier {
104104 Timer ? _eventFetchTimer;
105105 Timer ? _reconnectTimer;
106106 Timer ? _cooldownTicker;
107+ Timer ? _liveDataWatchdog;
108+
109+ /// Last successful [/ws/live] payload, or null until the first frame after subscribe.
110+ DateTime ? _lastLiveStateAt;
111+
112+ /// When the current live WebSocket subscription was opened.
113+ DateTime ? _wsSubscribeAt;
114+
115+ /// Throttle bundled-engine kills from the stale-data watchdog.
116+ DateTime ? _lastBundledEngineKillAttempt;
117+
118+ static const Duration _liveDataStaleThreshold = Duration (seconds: 30 );
119+ static const Duration _liveDataWatchdogInterval = Duration (seconds: 8 );
120+ static const Duration _bundledEngineKillCooldown = Duration (seconds: 50 );
121+
122+ bool _recoveringLive = false ;
107123
108124 /// When true, [_connectionError] may hold a bootstrap failure; do not clear it in [_tryConnect] .
109125 bool _bootstrapErrorPending = false ;
@@ -118,6 +134,8 @@ class EngineProvider extends ChangeNotifier {
118134
119135 Future <void > reconnect () async {
120136 _alertFeedFromWs.clear ();
137+ _liveDataWatchdog? .cancel ();
138+ _liveDataWatchdog = null ;
121139 _liveSub? .cancel ();
122140 _alertSub? .cancel ();
123141 _processFetchTimer? .cancel ();
@@ -179,13 +197,17 @@ class EngineProvider extends ChangeNotifier {
179197 _liveSub = stream.listen (
180198 _onStateReceived,
181199 onError: (error) {
200+ _liveDataWatchdog? .cancel ();
201+ _liveDataWatchdog = null ;
182202 _connected = false ;
183203 _bootstrapErrorPending = false ;
184204 _connectionError = 'Connection lost. Retrying...' ;
185205 notifyListeners ();
186206 _scheduleReconnect ();
187207 },
188208 onDone: () {
209+ _liveDataWatchdog? .cancel ();
210+ _liveDataWatchdog = null ;
189211 _connected = false ;
190212 _bootstrapErrorPending = false ;
191213 _connectionError = 'Disconnected. Retrying...' ;
@@ -208,10 +230,19 @@ class EngineProvider extends ChangeNotifier {
208230 // Stay "disconnected" in UI until first live frame arrives (avoids false
209231 // positive if the socket dies immediately after subscribe).
210232 _connected = false ;
233+ _wsSubscribeAt = DateTime .now ();
234+ _lastLiveStateAt = null ;
235+ _liveDataWatchdog? .cancel ();
236+ _liveDataWatchdog = Timer .periodic (
237+ _liveDataWatchdogInterval,
238+ (_) => _checkLiveDataStale (),
239+ );
211240 notifyListeners ();
212241 unawaited (_fetchProcesses ());
213242 unawaited (_fetchEvents ());
214243 } catch (e) {
244+ _liveDataWatchdog? .cancel ();
245+ _liveDataWatchdog = null ;
215246 _connected = false ;
216247 _bootstrapErrorPending = false ;
217248 _connectionError = 'Cannot connect to engine. Is it running?' ;
@@ -220,9 +251,65 @@ class EngineProvider extends ChangeNotifier {
220251 }
221252 }
222253
254+ void _checkLiveDataStale () {
255+ if (_liveSub == null || _recoveringLive) return ;
256+ final reference = _lastLiveStateAt ?? _wsSubscribeAt;
257+ if (reference == null ) return ;
258+ if (DateTime .now ().difference (reference) < _liveDataStaleThreshold) {
259+ return ;
260+ }
261+ final lastKill = _lastBundledEngineKillAttempt;
262+ if (lastKill != null &&
263+ DateTime .now ().difference (lastKill) < _bundledEngineKillCooldown) {
264+ return ;
265+ }
266+ unawaited (_recoverStalledLiveChannel ());
267+ }
268+
269+ /// HTTP/WS up but no live telemetry (hung orchestrator): restart bundled engine
270+ /// on Windows, then bootstrap again.
271+ Future <void > _recoverStalledLiveChannel () async {
272+ if (_recoveringLive) return ;
273+ _recoveringLive = true ;
274+ _liveDataWatchdog? .cancel ();
275+ _liveDataWatchdog = null ;
276+ _reconnectTimer? .cancel ();
277+ _lastBundledEngineKillAttempt = DateTime .now ();
278+ try {
279+ _connected = false ;
280+ _connectionError = 'No live data from engine; restarting engine…' ;
281+ notifyListeners ();
282+
283+ if (Platform .isWindows &&
284+ EngineBundledLauncher .bundledEngineExecutablePath () != null ) {
285+ try {
286+ await Process .run (
287+ 'taskkill' ,
288+ const ['/F' , '/IM' , 'SentraCoreEngine.exe' ],
289+ runInShell: true ,
290+ );
291+ } catch (_) {
292+ // Process may already be gone.
293+ }
294+ await Future <void >.delayed (const Duration (seconds: 1 ));
295+ }
296+
297+ _liveSub? .cancel ();
298+ _alertSub? .cancel ();
299+ _processFetchTimer? .cancel ();
300+ _eventFetchTimer? .cancel ();
301+
302+ await _bootstrapAndConnect ();
303+ } finally {
304+ _recoveringLive = false ;
305+ }
306+ }
307+
223308 void _scheduleReconnect () {
224309 _reconnectTimer? .cancel ();
225310 _reconnectTimer = Timer (const Duration (seconds: 5 ), () {
311+ _liveDataWatchdog? .cancel ();
312+ _liveDataWatchdog = null ;
226313 _liveSub? .cancel ();
227314 _alertSub? .cancel ();
228315 _processFetchTimer? .cancel ();
@@ -292,6 +379,7 @@ class EngineProvider extends ChangeNotifier {
292379 }
293380
294381 void _onStateReceived (SystemState state) {
382+ _lastLiveStateAt = DateTime .now ();
295383 if (! _didPullEnginePrefs) {
296384 unawaited (_maybePullEnginePreferences ());
297385 }
@@ -358,6 +446,7 @@ class EngineProvider extends ChangeNotifier {
358446
359447 @override
360448 void dispose () {
449+ _liveDataWatchdog? .cancel ();
361450 _liveSub? .cancel ();
362451 _alertSub? .cancel ();
363452 _processFetchTimer? .cancel ();
0 commit comments