@@ -25,6 +25,9 @@ class AppDomain extends Domain {
25
25
26
26
final _appStates = < String , _AppState > {};
27
27
28
+ // Prevents duplicate stdout listeners for the same appId
29
+ final _activeListeners = < String > {};
30
+
28
31
// Mapping from service name to service method.
29
32
final Map <String , String > _registeredMethodsForService = < String , String > {};
30
33
@@ -85,24 +88,11 @@ class AppDomain extends Domain {
85
88
'deviceId' : 'chrome' ,
86
89
'launchMode' : 'run'
87
90
});
88
- // TODO(grouma) - limit the catch to the appropriate error.
89
- try {
90
- await vmService.streamCancel ('Stdout' );
91
- } catch (_) {}
92
- try {
93
- await vmService.streamListen ('Stdout' );
94
- } catch (_) {}
95
- try {
96
- vmService.onServiceEvent.listen (_onServiceEvent);
97
- await vmService.streamListen ('Service' );
98
- } catch (_) {}
91
+
92
+ // Set up VM service listeners (only once per appId to prevent duplicates)
99
93
// ignore: cancel_subscriptions
100
- final stdOutSub = vmService.onStdoutEvent.listen ((log) {
101
- sendEvent ('app.log' , {
102
- 'appId' : appId,
103
- 'log' : utf8.decode (base64.decode (log.bytes! )),
104
- });
105
- });
94
+ final stdOutSub = await _setupVmServiceListeners (appId, vmService);
95
+
106
96
sendEvent ('app.debugPort' , {
107
97
'appId' : appId,
108
98
'port' : debugConnection.port,
@@ -121,8 +111,7 @@ class AppDomain extends Domain {
121
111
appConnection.runMain ();
122
112
123
113
unawaited (debugConnection.onDone.whenComplete (() {
124
- appState.dispose ();
125
- _appStates.remove (appId);
114
+ _cleanupAppConnection (appId, appState);
126
115
}));
127
116
}
128
117
@@ -223,20 +212,59 @@ class AppDomain extends Domain {
223
212
return true ;
224
213
}
225
214
215
+ /// Sets up VM service listeners for the given appId if not already active.
216
+ /// Returns the stdout subscription if created, null otherwise.
217
+ Future <StreamSubscription <Event >?> _setupVmServiceListeners (
218
+ String appId, VmService vmService) async {
219
+ if (_activeListeners.contains (appId)) {
220
+ return null ; // Already listening for this appId
221
+ }
222
+
223
+ _activeListeners.add (appId);
224
+
225
+ // TODO(grouma) - limit the catch to the appropriate error.
226
+ try {
227
+ await vmService.streamCancel ('Stdout' );
228
+ } catch (_) {}
229
+ try {
230
+ await vmService.streamListen ('Stdout' );
231
+ } catch (_) {}
232
+ try {
233
+ vmService.onServiceEvent.listen (_onServiceEvent);
234
+ await vmService.streamListen ('Service' );
235
+ } catch (_) {}
236
+
237
+ // ignore: cancel_subscriptions
238
+ return vmService.onStdoutEvent.listen ((log) {
239
+ sendEvent ('app.log' , {
240
+ 'appId' : appId,
241
+ 'log' : utf8.decode (base64.decode (log.bytes! )),
242
+ });
243
+ });
244
+ }
245
+
246
+ /// Cleans up an app connection and its associated listeners.
247
+ void _cleanupAppConnection (String appId, _AppState appState) {
248
+ appState.dispose ();
249
+ _appStates.remove (appId);
250
+ _activeListeners.remove (appId);
251
+ }
252
+
226
253
@override
227
254
void dispose () {
228
255
_isShutdown = true ;
229
256
for (final state in _appStates.values) {
230
257
state.dispose ();
231
258
}
232
259
_appStates.clear ();
260
+ _activeListeners.clear ();
233
261
}
234
262
}
235
263
236
264
class _AppState {
237
265
final DebugConnection _debugConnection;
238
266
final StreamSubscription <BuildResult > _resultSub;
239
- final StreamSubscription <Event > _stdOutSub;
267
+ final StreamSubscription <Event >? _stdOutSub;
240
268
241
269
bool _isDisposed = false ;
242
270
@@ -247,7 +275,7 @@ class _AppState {
247
275
void dispose () {
248
276
if (_isDisposed) return ;
249
277
_isDisposed = true ;
250
- _stdOutSub.cancel ();
278
+ _stdOutSub? .cancel ();
251
279
_resultSub.cancel ();
252
280
_debugConnection.close ();
253
281
}
0 commit comments