Skip to content

Fix duplicate connection/logs in Webdev #2635

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jun 20, 2025
4 changes: 3 additions & 1 deletion webdev/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
## 3.7.2-wip
## 3.7.2

- Fixed duplicate app logs on page refresh by preventing multiple stdout listeners for the same appId.
- Adds `--offline` flag [#2483](https://github.com/dart-lang/webdev/pull/2483).
- Support the `--hostname` flag when the `--tls-cert-key` and `--tls-cert-chain` flags are present [#2588](https://github.com/dart-lang/webdev/pull/2588).
- Update `dwds` constraint to `24.3.11`.

## 3.7.1

Expand Down
87 changes: 57 additions & 30 deletions webdev/lib/src/daemon/app_domain.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,46 +63,33 @@ class AppDomain extends Domain {

Future<void> _handleAppConnections(WebDevServer server) async {
final dwds = server.dwds!;

// The connection is established right before `main()` is called.
await for (final appConnection in dwds.connectedApps) {
final appId = appConnection.request.appId;

// Check if we already have an active app state for this appId
if (_appStates.containsKey(appId)) {
// Reuse existing connection, just run main again
appConnection.runMain();
continue;
}

final debugConnection = await dwds.debugConnection(appConnection);
final debugUri = debugConnection.ddsUri ?? debugConnection.uri;
final vmService = await vmServiceConnectUri(debugUri);
final appId = appConnection.request.appId;
unawaited(debugConnection.onDone.then((_) {
sendEvent('app.log', {
'appId': appId,
'log': 'Lost connection to device.',
});
sendEvent('app.stop', {
'appId': appId,
});
daemon.shutdown();
}));

sendEvent('app.start', {
'appId': appId,
'directory': Directory.current.path,
'deviceId': 'chrome',
'launchMode': 'run'
});
// TODO(grouma) - limit the catch to the appropriate error.
try {
await vmService.streamCancel('Stdout');
} catch (_) {}
try {
await vmService.streamListen('Stdout');
} catch (_) {}
try {
vmService.onServiceEvent.listen(_onServiceEvent);
await vmService.streamListen('Service');
} catch (_) {}

// Set up VM service listeners for this appId
// ignore: cancel_subscriptions
final stdOutSub = vmService.onStdoutEvent.listen((log) {
sendEvent('app.log', {
'appId': appId,
'log': utf8.decode(base64.decode(log.bytes!)),
});
});
final stdOutSub = await _setupVmServiceListeners(appId, vmService);

sendEvent('app.debugPort', {
'appId': appId,
'port': debugConnection.port,
Expand All @@ -120,9 +107,19 @@ class AppDomain extends Domain {

appConnection.runMain();

// Handle connection termination - send events first, then cleanup
unawaited(debugConnection.onDone.whenComplete(() {
appState.dispose();
_appStates.remove(appId);
sendEvent('app.log', {
'appId': appId,
'log': 'Lost connection to device.',
});
sendEvent('app.stop', {
'appId': appId,
});
daemon.shutdown();

// Clean up app resources
_cleanupAppConnection(appId, appState);
}));
}

Expand Down Expand Up @@ -223,6 +220,36 @@ class AppDomain extends Domain {
return true;
}

/// Sets up VM service listeners for the given appId.
/// Returns the stdout subscription.
Future<StreamSubscription<Event>> _setupVmServiceListeners(
String appId, VmService vmService) async {
try {
vmService.onServiceEvent.listen(_onServiceEvent);
await vmService.streamListen(EventStreams.kService);
} catch (_) {}

// ignore: cancel_subscriptions
final stdoutSubscription = vmService.onStdoutEvent.listen((log) {
sendEvent('app.log', {
'appId': appId,
'log': utf8.decode(base64.decode(log.bytes!)),
});
});

try {
await vmService.streamListen(EventStreams.kStdout);
} catch (_) {}

return stdoutSubscription;
}

/// Cleans up an app connection and its associated listeners.
void _cleanupAppConnection(String appId, _AppState appState) {
appState.dispose();
_appStates.remove(appId);
}

@override
void dispose() {
_isShutdown = true;
Expand Down
2 changes: 1 addition & 1 deletion webdev/lib/src/version.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions webdev/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: webdev
# Every time this changes you need to run `dart run build_runner build`.
version: 3.7.2-wip
version: 3.7.2
# We should not depend on a dev SDK before publishing.
# publish_to: none
description: >-
Expand All @@ -19,7 +19,7 @@ dependencies:
crypto: ^3.0.2
dds: ^4.1.0
# Pin DWDS to avoid dependency conflicts with vm_service:
dwds: 24.3.5
dwds: 24.3.11
http: ^1.0.0
http_multi_server: ^3.2.0
io: ^1.0.3
Expand Down
Loading