From c186915864befe567ba8bb486e0eecc254b836e6 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Mon, 20 Oct 2025 11:42:44 -0400 Subject: [PATCH 1/5] Disable screens not compatible with DWDS websocket mode The DWDS websocket mode used by the Flutter `web-server` device provides a limited subset of the service protocol that doesn't include debugging capabilities, expression evaluation support, or object inspection. This change adds detection for applications running in this limited mode and hides screens that rely on unsupported functionality (e.g., debugger). --- .../lib/src/shared/framework/screen.dart | 84 +++++++++++-------- .../lib/src/service/connected_app.dart | 35 ++++---- 2 files changed, 67 insertions(+), 52 deletions(-) diff --git a/packages/devtools_app/lib/src/shared/framework/screen.dart b/packages/devtools_app/lib/src/shared/framework/screen.dart index fcacc5626b9..33a0e45f831 100644 --- a/packages/devtools_app/lib/src/shared/framework/screen.dart +++ b/packages/devtools_app/lib/src/shared/framework/screen.dart @@ -49,6 +49,7 @@ enum ScreenMetaData { title: 'Performance', iconAsset: 'icons/app_bar/performance.png', worksWithOfflineData: true, + requiresWebAppDebuggable: true, requiresConnection: false, tutorialVideoTimestamp: '?t=261', ), @@ -75,6 +76,7 @@ enum ScreenMetaData { title: 'Debugger', icon: Octicons.bug, requiresDebugBuild: true, + requiresWebAppDebuggable: true, tutorialVideoTimestamp: '?t=513', ), network( @@ -120,6 +122,7 @@ enum ScreenMetaData { title: 'VM Tools', icon: Icons.settings_applications, requiresAdvancedDeveloperMode: true, + requiresWebAppDebuggable: true, ), dtdTools( 'dtdTools', @@ -140,6 +143,7 @@ enum ScreenMetaData { this.requiresFlutter = false, this.requiresDebugBuild = false, this.requiresAdvancedDeveloperMode = false, + this.requiresWebAppDebuggable = false, this.worksWithOfflineData = false, this.requiresLibrary, this.tutorialVideoTimestamp, @@ -157,6 +161,7 @@ enum ScreenMetaData { final bool requiresFlutter; final bool requiresDebugBuild; final bool requiresAdvancedDeveloperMode; + final bool requiresWebAppDebuggable; final bool worksWithOfflineData; final String? requiresLibrary; @@ -206,6 +211,7 @@ abstract class Screen { this.requiresFlutter = false, this.requiresDebugBuild = false, this.requiresAdvancedDeveloperMode = false, + this.requiresWebAppDebuggable = false, this.worksWithOfflineData = false, this.showFloatingDebuggerControls = true, }) : assert( @@ -225,6 +231,7 @@ abstract class Screen { bool requiresFlutter = false, bool requiresDebugBuild = false, bool requiresAdvancedDeveloperMode = false, + bool requiresWebAppDebuggable = false, bool worksWithOfflineData = false, bool Function(FlutterVersion? currentVersion)? shouldShowForFlutterVersion, bool showFloatingDebuggerControls = true, @@ -241,6 +248,7 @@ abstract class Screen { requiresFlutter: requiresFlutter, requiresDebugBuild: requiresDebugBuild, requiresAdvancedDeveloperMode: requiresAdvancedDeveloperMode, + requiresWebAppDebuggable: requiresWebAppDebuggable, worksWithOfflineData: worksWithOfflineData, showFloatingDebuggerControls: showFloatingDebuggerControls, title: title, @@ -264,6 +272,7 @@ abstract class Screen { requiresFlutter: metadata.requiresFlutter, requiresDebugBuild: metadata.requiresDebugBuild, requiresAdvancedDeveloperMode: metadata.requiresAdvancedDeveloperMode, + requiresWebAppDebuggable: metadata.requiresWebAppDebuggable, worksWithOfflineData: metadata.worksWithOfflineData, shouldShowForFlutterVersion: shouldShowForFlutterVersion, showFloatingDebuggerControls: showFloatingDebuggerControls, @@ -341,6 +350,10 @@ abstract class Screen { /// is enabled. final bool requiresAdvancedDeveloperMode; + /// Whether this screen should only be included when the app is a web app with full debugging + /// support. + final bool requiresWebAppDebuggable; + /// Whether this screen works offline and should show in offline mode even if conditions are not met. final bool worksWithOfflineData; @@ -512,9 +525,11 @@ abstract class Screen { } } + final serviceManager = serviceConnection.serviceManager; + final connectedApp = serviceManager.connectedApp; final serviceReady = - serviceConnection.serviceManager.isServiceAvailable && - serviceConnection.serviceManager.connectedApp!.connectedAppInitialized; + serviceManager.isServiceAvailable && + connectedApp!.connectedAppInitialized; if (!serviceReady) { if (!screen.requiresConnection) { _log.finest('screen does not require connection: returning true'); @@ -531,42 +546,42 @@ abstract class Screen { } } - if (screen.requiresLibrary != null) { - if (serviceConnection.serviceManager.isolateManager.mainIsolate.value == - null || - !serviceConnection.serviceManager.libraryUriAvailableNow( - screen.requiresLibrary, - )) { - _log.finest( - 'screen requires library ${screen.requiresLibrary}: returning false', - ); - return ( - show: false, - disabledReason: ScreenDisabledReason.requiresDartLibrary, - ); - } + if (screen.requiresLibrary != null && + (serviceManager.isolateManager.mainIsolate.value == null || + !serviceManager.libraryUriAvailableNow(screen.requiresLibrary))) { + _log.finest( + 'screen requires library ${screen.requiresLibrary}: returning false', + ); + return ( + show: false, + disabledReason: ScreenDisabledReason.requiresDartLibrary, + ); } - if (screen.requiresDartVm) { - if (serviceConnection.serviceManager.connectedApp!.isRunningOnDartVM != - true) { - _log.finest('screen requires Dart VM: returning false'); - return (show: false, disabledReason: ScreenDisabledReason.requiresDartVm); - } + if (screen.requiresDartVm && connectedApp.isRunningOnDartVM != true) { + _log.finest('screen requires Dart VM: returning false'); + return (show: false, disabledReason: ScreenDisabledReason.requiresDartVm); } - if (screen.requiresFlutter && - serviceConnection.serviceManager.connectedApp!.isFlutterAppNow == false) { + if (screen.requiresFlutter && connectedApp.isFlutterAppNow == false) { _log.finest('screen requires Flutter: returning false'); return (show: false, disabledReason: ScreenDisabledReason.requiresFlutter); } - if (screen.requiresDebugBuild) { - if (serviceConnection.serviceManager.connectedApp!.isProfileBuildNow == - true) { - _log.finest('screen requires debug build: returning false'); - return ( - show: false, - disabledReason: ScreenDisabledReason.requiresDebugBuild, - ); - } + if (screen.requiresDebugBuild && connectedApp.isProfileBuildNow == true) { + _log.finest('screen requires debug build: returning false'); + return ( + show: false, + disabledReason: ScreenDisabledReason.requiresDebugBuild, + ); + } + if (screen.requiresWebAppDebuggable && + connectedApp.isDartWebAppNow == true && + !connectedApp.isDebuggableWebApp) { + _log.finest( + 'screen requires a debuggable web application: returning false', + ); + return ( + show: false, + disabledReason: ScreenDisabledReason.requiresDebuggableWebApp, + ); } _log.finest('${screen.screenId} screen supported: returning true'); return (show: true, disabledReason: null); @@ -644,6 +659,9 @@ enum ScreenDisabledReason { requiresAdvancedDeveloperMode( 'only works when Advanced Developer Mode is enabled', ), + requiresDebuggableWebApp( + 'only works with web applications with full debugging support.', + ), serviceNotReady( 'requires a connected application, but there is no connection available.', ); diff --git a/packages/devtools_app_shared/lib/src/service/connected_app.dart b/packages/devtools_app_shared/lib/src/service/connected_app.dart index 96d8cca0658..3e3af26f384 100644 --- a/packages/devtools_app_shared/lib/src/service/connected_app.dart +++ b/packages/devtools_app_shared/lib/src/service/connected_app.dart @@ -99,7 +99,22 @@ class ConnectedApp { bool get isDebugFlutterAppNow => isFlutterAppNow! && !isProfileBuildNow!; - bool? get isRunningOnDartVM => serviceManager!.vm!.name != 'ChromeDebugProxy'; + /// Returns true is the connected application's VM has the name + /// [dwdsChromeDebugProxyDeviceName], indicating that DWDS has an active + /// Chrome debugger connection with support for expression evaluation, + /// object inspection, and setting breakpoints. + /// + /// If false, the connected application supports a reduced subset of the VM + /// service protocol. + bool get isDebuggableWebApp => + serviceManager!.vm!.name == dwdsChromeDebugProxyDeviceName; + + bool? get isRunningOnDartVM { + final name = serviceManager!.vm!.name; + // These are the two possible VM names returned by DWDS. + return name != dwdsChromeDebugProxyDeviceName && + name != dwdsWebSocketDebugProxyDeviceName; + } Future get isDartCliApp async => isRunningOnDartVM! && !(await isFlutterApp); @@ -126,24 +141,6 @@ class ConnectedApp { shouldLogError: false, ); return !(value?.kind == 'Bool'); - - // TODO(terry): Disabled below code, it will hang if flutter run --start-paused - // see issue https://github.com/flutter/devtools/issues/2082. - // Currently, if eval (see above) doesn't work then we're - // running in Profile mode. - /* - assert(serviceConnectionManager.isServiceAvailable); - // Only flutter apps have profile and non-profile builds. If this changes in - // the future (flutter web), we can modify this check. - if (!isRunningOnDartVM || !await isFlutterApp) return false; - - await serviceConnectionManager.manager.serviceExtensionManager.extensionStatesUpdated.future; - - // The debugAllowBanner extension is only available in debug builds - final hasDebugExtension = serviceConnectionManager.manager.serviceExtensionManager - .isServiceExtensionAvailable(extensions.debugAllowBanner.extension); - return !hasDebugExtension; - */ } Future initializeValues({void Function()? onComplete}) async { From 52b4c2156f1e6ac4b6a0973a9af57c8d3bf51e54 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Mon, 20 Oct 2025 12:26:22 -0400 Subject: [PATCH 2/5] Fix tests --- .../framework/visible_screens_test.dart | 51 +++++++++++++++++++ .../devtools_test/lib/src/mocks/mocks.dart | 4 ++ 2 files changed, 55 insertions(+) diff --git a/packages/devtools_app/test/shared/framework/visible_screens_test.dart b/packages/devtools_app/test/shared/framework/visible_screens_test.dart index 4e1a97c4470..2d2d5da458e 100644 --- a/packages/devtools_app/test/shared/framework/visible_screens_test.dart +++ b/packages/devtools_app/test/shared/framework/visible_screens_test.dart @@ -41,6 +41,7 @@ void main() { void setupMockConnectedApp({ bool web = false, + bool debuggableWeb = true, bool flutter = false, bool debugMode = true, SemanticVersion? flutterVersion, @@ -114,6 +115,31 @@ void main() { ); }); + testWidgets('are correct for Dart Web app (DWDS websocket mode)', ( + WidgetTester tester, + ) async { + setupMockConnectedApp(web: true, debuggableWeb: false); + + expect( + visibleScreenTypes, + equals([ + HomeScreen, + // InspectorScreen, + // LegacyPerformanceScreen, + // PerformanceScreen, + // ProfilerScreen, + // MemoryScreen, + // DebuggerScreen, + // NetworkScreen, + LoggingScreen, + // AppSizeScreen, + // DeepLinksScreen, + // VMDeveloperToolsScreen, + // DTDToolsScreen, + ]), + ); + }); + testWidgets('are correct for Flutter (non-web) debug app', ( WidgetTester tester, ) async { @@ -189,6 +215,31 @@ void main() { ); }); + testWidgets('are correct for Flutter web debug app (DWDS websocket mode)', ( + WidgetTester tester, + ) async { + setupMockConnectedApp(flutter: true, web: true, debuggableWeb: false); + + expect( + visibleScreenTypes, + equals([ + HomeScreen, + InspectorScreen, + // LegacyPerformanceScreen, + // PerformanceScreen, + // ProfilerScreen, + // MemoryScreen, + // DebuggerScreen, + // NetworkScreen, + LoggingScreen, + // AppSizeScreen, + // DeepLinksScreen, + // VMDeveloperToolsScreen, + // DTDToolsScreen, + ]), + ); + }); + testWidgets('are correct for Flutter app on old Flutter version', ( WidgetTester tester, ) async { diff --git a/packages/devtools_test/lib/src/mocks/mocks.dart b/packages/devtools_test/lib/src/mocks/mocks.dart index d2f2b10c98d..fee470e121e 100644 --- a/packages/devtools_test/lib/src/mocks/mocks.dart +++ b/packages/devtools_test/lib/src/mocks/mocks.dart @@ -146,6 +146,7 @@ void mockConnectedApp( bool isFlutterApp = true, bool isProfileBuild = false, bool isWebApp = false, + bool isDebuggableWebApp = true, String os = 'ios', String flutterVersion = '2.10.0', }) { @@ -160,6 +161,9 @@ void mockConnectedApp( when( connectedApp.isFlutterNativeAppNow, ).thenReturn(isFlutterApp && !isWebApp); + when( + connectedApp.isDebuggableWebApp, + ).thenReturn(isWebApp && isDebuggableWebApp); if (isFlutterApp) { when(connectedApp.flutterVersionNow).thenReturn( FlutterVersion.parse({ From 208443f53e947b9bb6f2f3cedaec721c7c6f683a Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Mon, 20 Oct 2025 12:39:38 -0400 Subject: [PATCH 3/5] Add missing parameter --- .../devtools_app/test/shared/framework/visible_screens_test.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/devtools_app/test/shared/framework/visible_screens_test.dart b/packages/devtools_app/test/shared/framework/visible_screens_test.dart index 2d2d5da458e..4cc7425d5cd 100644 --- a/packages/devtools_app/test/shared/framework/visible_screens_test.dart +++ b/packages/devtools_app/test/shared/framework/visible_screens_test.dart @@ -56,6 +56,7 @@ void main() { isFlutterApp: flutter, isProfileBuild: !debugMode, isWebApp: web, + isDebuggableWebApp: debuggableWeb, ); if (flutter) { fakeServiceConnection.serviceManager.availableLibraries.add( From c72eae38431c84f1a37dabdeeec0d3e8a608320d Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Tue, 21 Oct 2025 10:52:43 -0400 Subject: [PATCH 4/5] Address comments --- .../lib/src/shared/framework/screen.dart | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/devtools_app/lib/src/shared/framework/screen.dart b/packages/devtools_app/lib/src/shared/framework/screen.dart index fd62d2885e0..721fb1a019c 100644 --- a/packages/devtools_app/lib/src/shared/framework/screen.dart +++ b/packages/devtools_app/lib/src/shared/framework/screen.dart @@ -32,6 +32,7 @@ enum ScreenMetaData { 'home', iconAsset: 'icons/app_bar/devtools.png', requiresConnection: false, + supportsWebServerDevice: true, tutorialVideoTimestamp: '?t=0', ), inspector( @@ -40,6 +41,7 @@ enum ScreenMetaData { iconAsset: 'icons/app_bar/inspector.png', requiresFlutter: true, requiresDebugBuild: true, + supportsWebServerDevice: true, tutorialVideoTimestamp: '?t=172', ), performance( @@ -47,7 +49,6 @@ enum ScreenMetaData { title: 'Performance', iconAsset: 'icons/app_bar/performance.png', worksWithOfflineData: true, - requiresWebAppDebuggable: true, requiresConnection: false, tutorialVideoTimestamp: '?t=261', ), @@ -74,7 +75,6 @@ enum ScreenMetaData { title: 'Debugger', icon: Octicons.bug, requiresDebugBuild: true, - requiresWebAppDebuggable: true, tutorialVideoTimestamp: '?t=513', ), network( @@ -91,6 +91,7 @@ enum ScreenMetaData { 'logging', title: 'Logging', iconAsset: 'icons/app_bar/logging.png', + supportsWebServerDevice: true, tutorialVideoTimestamp: '?t=558', ), provider( @@ -120,7 +121,6 @@ enum ScreenMetaData { title: 'VM Tools', icon: Icons.settings_applications, requiresAdvancedDeveloperMode: true, - requiresWebAppDebuggable: true, ), dtdTools( 'dtdTools', @@ -141,7 +141,7 @@ enum ScreenMetaData { this.requiresFlutter = false, this.requiresDebugBuild = false, this.requiresAdvancedDeveloperMode = false, - this.requiresWebAppDebuggable = false, + this.supportsWebServerDevice = false, this.worksWithOfflineData = false, this.requiresLibrary, this.tutorialVideoTimestamp, @@ -159,7 +159,7 @@ enum ScreenMetaData { final bool requiresFlutter; final bool requiresDebugBuild; final bool requiresAdvancedDeveloperMode; - final bool requiresWebAppDebuggable; + final bool supportsWebServerDevice; final bool worksWithOfflineData; final String? requiresLibrary; @@ -209,7 +209,7 @@ abstract class Screen { this.requiresFlutter = false, this.requiresDebugBuild = false, this.requiresAdvancedDeveloperMode = false, - this.requiresWebAppDebuggable = false, + this.supportsWebServerDevice = false, this.worksWithOfflineData = false, this.showFloatingDebuggerControls = true, }) : assert( @@ -229,7 +229,7 @@ abstract class Screen { bool requiresFlutter = false, bool requiresDebugBuild = false, bool requiresAdvancedDeveloperMode = false, - bool requiresWebAppDebuggable = false, + bool supportsWebServerDevice = false, bool worksWithOfflineData = false, bool Function(FlutterVersion? currentVersion)? shouldShowForFlutterVersion, bool showFloatingDebuggerControls = true, @@ -246,7 +246,7 @@ abstract class Screen { requiresFlutter: requiresFlutter, requiresDebugBuild: requiresDebugBuild, requiresAdvancedDeveloperMode: requiresAdvancedDeveloperMode, - requiresWebAppDebuggable: requiresWebAppDebuggable, + supportsWebServerDevice: supportsWebServerDevice, worksWithOfflineData: worksWithOfflineData, showFloatingDebuggerControls: showFloatingDebuggerControls, title: title, @@ -270,7 +270,7 @@ abstract class Screen { requiresFlutter: metadata.requiresFlutter, requiresDebugBuild: metadata.requiresDebugBuild, requiresAdvancedDeveloperMode: metadata.requiresAdvancedDeveloperMode, - requiresWebAppDebuggable: metadata.requiresWebAppDebuggable, + supportsWebServerDevice: metadata.supportsWebServerDevice, worksWithOfflineData: metadata.worksWithOfflineData, shouldShowForFlutterVersion: shouldShowForFlutterVersion, showFloatingDebuggerControls: showFloatingDebuggerControls, @@ -350,7 +350,7 @@ abstract class Screen { /// Whether this screen should only be included when the app is a web app with full debugging /// support. - final bool requiresWebAppDebuggable; + final bool supportsWebServerDevice; /// Whether this screen works offline and should show in offline mode even if conditions are not met. final bool worksWithOfflineData; @@ -550,7 +550,7 @@ abstract class Screen { disabledReason: ScreenDisabledReason.requiresDebugBuild, ); } - if (screen.requiresWebAppDebuggable && + if (!screen.supportsWebServerDevice && connectedApp.isDartWebAppNow == true && !connectedApp.isDebuggableWebApp) { _log.finest( From 72f89400e243f4c25b0183e32bdf5bb9aa6b93aa Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Thu, 23 Oct 2025 09:08:23 -0400 Subject: [PATCH 5/5] Clarify web app debugging support in comments Updated comment to clarify web app debugging support. --- packages/devtools_app/lib/src/shared/framework/screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devtools_app/lib/src/shared/framework/screen.dart b/packages/devtools_app/lib/src/shared/framework/screen.dart index 721fb1a019c..c3b49317b6f 100644 --- a/packages/devtools_app/lib/src/shared/framework/screen.dart +++ b/packages/devtools_app/lib/src/shared/framework/screen.dart @@ -348,7 +348,7 @@ abstract class Screen { /// is enabled. final bool requiresAdvancedDeveloperMode; - /// Whether this screen should only be included when the app is a web app with full debugging + /// Whether this screen should be included when the app is a web app without full debugging /// support. final bool supportsWebServerDevice;