From 7477d7ac79debc06e92a72cec3f59178c74bc44d Mon Sep 17 00:00:00 2001
From: Ben Konyi <bkonyi@google.com>
Date: Mon, 30 Jan 2023 16:00:18 -0500
Subject: [PATCH] Reland "Add --serve-observatory flag to run, attach, and test
 (#118402)" (#119529)

* Reland "Add --serve-observatory flag to run, attach, and test (#118402)"

This reverts commit 86ab01d2bd82333be7a0cd4957903f424de02104.

* Fix flaky failures

* Fix VM service disappearing failure
---
 packages/flutter_tools/lib/src/base/dds.dart  |  4 ++
 .../lib/src/commands/attach.dart              |  4 ++
 .../flutter_tools/lib/src/commands/run.dart   |  2 +
 .../flutter_tools/lib/src/commands/test.dart  |  2 +
 packages/flutter_tools/lib/src/device.dart    |  8 ++-
 .../lib/src/resident_devtools_handler.dart    | 14 ++++
 .../lib/src/resident_runner.dart              | 20 ++++++
 packages/flutter_tools/lib/src/run_cold.dart  | 34 ++++++----
 packages/flutter_tools/lib/src/run_hot.dart   |  4 ++
 .../lib/src/runner/flutter_command.dart       |  8 +++
 .../lib/src/test/flutter_tester_device.dart   | 11 ++-
 .../resident_devtools_handler_test.dart       | 53 +++++++++++++--
 .../general.shard/resident_runner_test.dart   | 35 +++++++++-
 .../flutter_attach_test.dart                  | 62 +++++++++++++++++
 .../test/integration.shard/test_driver.dart   |  4 ++
 .../test/integration.shard/test_test.dart     | 67 +++++++++++++++++++
 16 files changed, 312 insertions(+), 20 deletions(-)

diff --git a/packages/flutter_tools/lib/src/base/dds.dart b/packages/flutter_tools/lib/src/base/dds.dart
index 00ddabd741..3c20d98a23 100644
--- a/packages/flutter_tools/lib/src/base/dds.dart
+++ b/packages/flutter_tools/lib/src/base/dds.dart
@@ -96,4 +96,8 @@ class DartDevelopmentService {
   }
 
   Future<void> shutdown() async => _ddsInstance?.shutdown();
+
+  void setExternalDevToolsUri(Uri uri) {
+    _ddsInstance?.setExternalDevToolsUri(uri);
+  }
 }
diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart
index 9f736941da..438681b5d9 100644
--- a/packages/flutter_tools/lib/src/commands/attach.dart
+++ b/packages/flutter_tools/lib/src/commands/attach.dart
@@ -139,6 +139,7 @@ class AttachCommand extends FlutterCommand {
     usesTrackWidgetCreation(verboseHelp: verboseHelp);
     addDdsOptions(verboseHelp: verboseHelp);
     addDevToolsOptions(verboseHelp: verboseHelp);
+    addServeObservatoryOptions(verboseHelp: verboseHelp);
     usesDeviceTimeoutOption();
   }
 
@@ -200,6 +201,8 @@ known, it can be explicitly provided to attach via the command-line, e.g.
     return uri;
   }
 
+  bool get serveObservatory => boolArg('serve-observatory') ?? false;
+
   String? get appId {
     return stringArgDeprecated('app-id');
   }
@@ -514,6 +517,7 @@ known, it can be explicitly provided to attach via the command-line, e.g.
       enableDds: enableDds,
       ddsPort: ddsPort,
       devToolsServerAddress: devToolsServerAddress,
+      serveObservatory: serveObservatory,
     );
 
     return buildInfo.isDebug
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index 2fc0003e72..d56b3a03d6 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -179,6 +179,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment
     usesDeviceTimeoutOption();
     addDdsOptions(verboseHelp: verboseHelp);
     addDevToolsOptions(verboseHelp: verboseHelp);
+    addServeObservatoryOptions(verboseHelp: verboseHelp);
     addAndroidSpecificBuildOptions(hide: !verboseHelp);
     usesFatalWarningsOption(verboseHelp: verboseHelp);
     addEnableImpellerFlag(verboseHelp: verboseHelp);
@@ -279,6 +280,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment
         nativeNullAssertions: boolArgDeprecated('native-null-assertions'),
         enableImpeller: enableImpeller,
         uninstallFirst: uninstallFirst,
+        serveObservatory: boolArgDeprecated('serve-observatory'),
         enableDartProfiling: enableDartProfiling,
       );
     }
diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart
index 9dc9b7e150..7c5c774c7f 100644
--- a/packages/flutter_tools/lib/src/commands/test.dart
+++ b/packages/flutter_tools/lib/src/commands/test.dart
@@ -215,6 +215,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
               'or as the string "none" to disable the timeout entirely.',
       );
     addDdsOptions(verboseHelp: verboseHelp);
+    addServeObservatoryOptions(verboseHelp: verboseHelp);
     usesFatalWarningsOption(verboseHelp: verboseHelp);
   }
 
@@ -404,6 +405,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
       buildInfo,
       startPaused: startPaused,
       disableServiceAuthCodes: boolArgDeprecated('disable-service-auth-codes'),
+      serveObservatory: boolArgDeprecated('serve-observatory'),
       // On iOS >=14, keeping this enabled will leave a prompt on the screen.
       disablePortPublication: true,
       enableDds: enableDds,
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index fbd2686eea..55ec8dab63 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -754,6 +754,7 @@ class DebuggingOptions {
     this.nativeNullAssertions = false,
     this.enableImpeller = false,
     this.uninstallFirst = false,
+    this.serveObservatory = true,
     this.enableDartProfiling = true,
    }) : debuggingEnabled = true;
 
@@ -799,7 +800,8 @@ class DebuggingOptions {
       fastStart = false,
       webEnableExpressionEvaluation = false,
       nullAssertions = false,
-      nativeNullAssertions = false;
+      nativeNullAssertions = false,
+      serveObservatory = false;
 
   DebuggingOptions._({
     required this.buildInfo,
@@ -844,6 +846,7 @@ class DebuggingOptions {
     required this.nativeNullAssertions,
     required this.enableImpeller,
     required this.uninstallFirst,
+    required this.serveObservatory,
     required this.enableDartProfiling,
   });
 
@@ -880,6 +883,7 @@ class DebuggingOptions {
   final bool webUseSseForDebugBackend;
   final bool webUseSseForInjectedClient;
   final bool enableImpeller;
+  final bool serveObservatory;
   final bool enableDartProfiling;
 
   /// Whether the tool should try to uninstall a previously installed version of the app.
@@ -1008,6 +1012,7 @@ class DebuggingOptions {
     'nullAssertions': nullAssertions,
     'nativeNullAssertions': nativeNullAssertions,
     'enableImpeller': enableImpeller,
+    'serveObservatory': serveObservatory,
     'enableDartProfiling': enableDartProfiling,
   };
 
@@ -1055,6 +1060,7 @@ class DebuggingOptions {
       nativeNullAssertions: json['nativeNullAssertions']! as bool,
       enableImpeller: (json['enableImpeller'] as bool?) ?? false,
       uninstallFirst: (json['uninstallFirst'] as bool?) ?? false,
+      serveObservatory: (json['serveObservatory'] as bool?) ?? false,
       enableDartProfiling: (json['enableDartProfiling'] as bool?) ?? true,
     );
 }
diff --git a/packages/flutter_tools/lib/src/resident_devtools_handler.dart b/packages/flutter_tools/lib/src/resident_devtools_handler.dart
index 6f3ccb61e9..0d210fd461 100644
--- a/packages/flutter_tools/lib/src/resident_devtools_handler.dart
+++ b/packages/flutter_tools/lib/src/resident_devtools_handler.dart
@@ -91,12 +91,26 @@ class FlutterResidentDevtoolsHandler implements ResidentDevtoolsHandler {
     final List<FlutterDevice?> devicesWithExtension = await _devicesWithExtensions(flutterDevices);
     await _maybeCallDevToolsUriServiceExtension(devicesWithExtension);
     await _callConnectedVmServiceUriExtension(devicesWithExtension);
+
     if (_shutdown) {
       // If we're shutting down, no point reporting the debugger list.
       return;
     }
     _readyToAnnounce = true;
     assert(_devToolsLauncher!.activeDevToolsServer != null);
+
+    final Uri? devToolsUrl = _devToolsLauncher!.devToolsUrl;
+    if (devToolsUrl != null) {
+      for (final FlutterDevice? device in devicesWithExtension) {
+        if (device == null) {
+          continue;
+        }
+        // Notify the DDS instances that there's a DevTools instance available so they can correctly
+        // redirect DevTools related requests.
+        device.device?.dds.setExternalDevToolsUri(devToolsUrl);
+      }
+    }
+
     if (_residentRunner.reportedDebuggers) {
       // Since the DevTools only just became available, we haven't had a chance to
       // report their URLs yet. Do so now.
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index dccdd2a6a4..6990128198 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -1402,6 +1402,26 @@ abstract class ResidentRunner extends ResidentHandlers {
     _finished.complete(0);
   }
 
+  Future<void> enableObservatory() async {
+    assert(debuggingOptions.serveObservatory);
+    final List<Future<vm_service.Response?>> serveObservatoryRequests = <Future<vm_service.Response?>>[];
+    for (final FlutterDevice? device in flutterDevices) {
+      if (device == null) {
+        continue;
+      }
+      // Notify the VM service if the user wants Observatory to be served.
+      serveObservatoryRequests.add(
+        device.vmService?.callMethodWrapper('_serveObservatory') ??
+          Future<vm_service.Response?>.value(),
+      );
+    }
+    try {
+      await Future.wait(serveObservatoryRequests);
+    } on vm_service.RPCError catch(e) {
+      globals.printWarning('Unable to enable Observatory: $e');
+    }
+  }
+
   void appFinished() {
     if (_finished.isCompleted) {
       return;
diff --git a/packages/flutter_tools/lib/src/run_cold.dart b/packages/flutter_tools/lib/src/run_cold.dart
index 4757763464..a8ba6cfb9c 100644
--- a/packages/flutter_tools/lib/src/run_cold.dart
+++ b/packages/flutter_tools/lib/src/run_cold.dart
@@ -80,12 +80,17 @@ class ColdRunner extends ResidentRunner {
       }
     }
 
-    if (enableDevTools && debuggingEnabled) {
-      // The method below is guaranteed never to return a failing future.
-      unawaited(residentDevtoolsHandler!.serveAndAnnounceDevTools(
-        devToolsServerAddress: debuggingOptions.devToolsServerAddress,
-        flutterDevices: flutterDevices,
-      ));
+    if (debuggingEnabled) {
+      if (enableDevTools) {
+        // The method below is guaranteed never to return a failing future.
+        unawaited(residentDevtoolsHandler!.serveAndAnnounceDevTools(
+          devToolsServerAddress: debuggingOptions.devToolsServerAddress,
+          flutterDevices: flutterDevices,
+        ));
+      }
+      if (debuggingOptions.serveObservatory) {
+        await enableObservatory();
+      }
     }
 
     if (flutterDevices.first.observatoryUris != null) {
@@ -162,12 +167,17 @@ class ColdRunner extends ResidentRunner {
       }
     }
 
-    if (enableDevTools && debuggingEnabled) {
-      // The method below is guaranteed never to return a failing future.
-      unawaited(residentDevtoolsHandler!.serveAndAnnounceDevTools(
-        devToolsServerAddress: debuggingOptions.devToolsServerAddress,
-        flutterDevices: flutterDevices,
-      ));
+    if (debuggingEnabled) {
+      if (enableDevTools) {
+        // The method below is guaranteed never to return a failing future.
+        unawaited(residentDevtoolsHandler!.serveAndAnnounceDevTools(
+          devToolsServerAddress: debuggingOptions.devToolsServerAddress,
+          flutterDevices: flutterDevices,
+        ));
+      }
+      if (debuggingOptions.serveObservatory) {
+        await enableObservatory();
+      }
     }
 
     appStartedCompleter?.complete();
diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart
index 1a89edd86a..31fe164e32 100644
--- a/packages/flutter_tools/lib/src/run_hot.dart
+++ b/packages/flutter_tools/lib/src/run_hot.dart
@@ -237,6 +237,10 @@ class HotRunner extends ResidentRunner {
       return 2;
     }
 
+    if (debuggingOptions.serveObservatory) {
+      await enableObservatory();
+    }
+
     if (enableDevTools) {
       // The method below is guaranteed never to return a failing future.
       unawaited(residentDevtoolsHandler!.serveAndAnnounceDevTools(
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index d67b80e7ff..e8c8955861 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -458,6 +458,14 @@ abstract class FlutterCommand extends Command<void> {
     );
   }
 
+  void addServeObservatoryOptions({required bool verboseHelp}) {
+    argParser.addFlag('serve-observatory',
+      hide: !verboseHelp,
+      defaultsTo: true,
+      help: 'Serve the legacy Observatory developer tooling through the VM service.',
+    );
+  }
+
   late final bool enableDds = () {
     bool ddsEnabled = false;
     if (argResults?.wasParsed('disable-dds') ?? false) {
diff --git a/packages/flutter_tools/lib/src/test/flutter_tester_device.dart b/packages/flutter_tools/lib/src/test/flutter_tester_device.dart
index 2e9e54fe4e..9b09f12642 100644
--- a/packages/flutter_tools/lib/src/test/flutter_tester_device.dart
+++ b/packages/flutter_tools/lib/src/test/flutter_tester_device.dart
@@ -11,6 +11,7 @@ import 'package:dds/dds.dart';
 import 'package:meta/meta.dart';
 import 'package:process/process.dart';
 import 'package:stream_channel/stream_channel.dart';
+import 'package:vm_service/vm_service.dart' as vm_service;
 
 import '../base/file_system.dart';
 import '../base/io.dart';
@@ -180,8 +181,15 @@ class FlutterTesterTestDevice extends TestDevice {
           compileExpression: compileExpression,
           logger: logger,
         );
-        unawaited(localVmService.then((FlutterVmService vmservice) {
+        unawaited(localVmService.then((FlutterVmService vmservice) async {
           logger.printTrace('test $id: Successfully connected to service protocol: $forwardingUri');
+          if (debuggingOptions.serveObservatory) {
+            try {
+              await vmservice.callMethodWrapper('_serveObservatory');
+            } on vm_service.RPCError {
+              logger.printWarning('Unable to enable Observatory');
+            }
+          }
         }));
 
         if (debuggingOptions.startPaused && !machine!) {
@@ -190,6 +198,7 @@ class FlutterTesterTestDevice extends TestDevice {
           logger.printStatus('  $forwardingUri');
           logger.printStatus('You should first set appropriate breakpoints, then resume the test in the debugger.');
         }
+
         _gotProcessObservatoryUri.complete(forwardingUri);
       },
     );
diff --git a/packages/flutter_tools/test/general.shard/resident_devtools_handler_test.dart b/packages/flutter_tools/test/general.shard/resident_devtools_handler_test.dart
index e3d3fcc983..171030cec1 100644
--- a/packages/flutter_tools/test/general.shard/resident_devtools_handler_test.dart
+++ b/packages/flutter_tools/test/general.shard/resident_devtools_handler_test.dart
@@ -4,6 +4,7 @@
 
 import 'dart:async';
 
+import 'package:flutter_tools/src/base/dds.dart';
 import 'package:flutter_tools/src/base/logger.dart';
 import 'package:flutter_tools/src/build_info.dart';
 import 'package:flutter_tools/src/cache.dart';
@@ -110,7 +111,9 @@ void main() {
 
   testWithoutContext('serveAndAnnounceDevTools with attached device does not fail on null vm service', () async {
     final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
-      FakeDevtoolsLauncher()..activeDevToolsServer = DevToolsServerAddress('localhost', 8080),
+      FakeDevtoolsLauncher()
+        ..activeDevToolsServer = DevToolsServerAddress('localhost', 8080)
+        ..devToolsUrl = Uri.parse('http://localhost:8080'),
       FakeResidentRunner(),
       BufferLogger.test(),
     );
@@ -125,7 +128,9 @@ void main() {
 
   testWithoutContext('serveAndAnnounceDevTools with invokes devtools and vm_service setter', () async {
     final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
-      FakeDevtoolsLauncher()..activeDevToolsServer = DevToolsServerAddress('localhost', 8080),
+      FakeDevtoolsLauncher()
+        ..activeDevToolsServer = DevToolsServerAddress('localhost', 8080)
+        ..devToolsUrl = Uri.parse('http://localhost:8080'),
       FakeResidentRunner(),
       BufferLogger.test(),
     );
@@ -194,7 +199,9 @@ void main() {
 
   testWithoutContext('serveAndAnnounceDevTools with web device', () async {
     final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
-      FakeDevtoolsLauncher()..activeDevToolsServer = DevToolsServerAddress('localhost', 8080),
+      FakeDevtoolsLauncher()
+        ..activeDevToolsServer = DevToolsServerAddress('localhost', 8080)
+        ..devToolsUrl = Uri.parse('http://localhost:8080'),
       FakeResidentRunner(),
       BufferLogger.test(),
     );
@@ -278,7 +285,9 @@ void main() {
 
   testWithoutContext('serveAndAnnounceDevTools with multiple devices and VM service disappears on one', () async {
     final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
-      FakeDevtoolsLauncher()..activeDevToolsServer = DevToolsServerAddress('localhost', 8080),
+      FakeDevtoolsLauncher()
+        ..activeDevToolsServer = DevToolsServerAddress('localhost', 8080)
+        ..devToolsUrl = Uri.parse('http://localhost:8080'),
       FakeResidentRunner(),
       BufferLogger.test(),
     );
@@ -442,6 +451,9 @@ class FakeResidentRunner extends Fake implements ResidentRunner {
 
   @override
   bool reportedDebuggers = false;
+
+  @override
+  DebuggingOptions debuggingOptions = DebuggingOptions.disabled(BuildInfo.debug);
 }
 
 class FakeFlutterDevice extends Fake implements FlutterDevice {
@@ -458,4 +470,35 @@ class FakeFlutterDevice extends Fake implements FlutterDevice {
 // Unfortunately Device, despite not being immutable, has an `operator ==`.
 // Until we fix that, we have to also ignore related lints here.
 // ignore: avoid_implementing_value_types
-class FakeDevice extends Fake implements Device { }
+class FakeDevice extends Fake implements Device {
+  @override
+  DartDevelopmentService get dds => FakeDartDevelopmentService();
+}
+
+class FakeDartDevelopmentService extends Fake implements DartDevelopmentService {
+  bool started = false;
+  bool disposed = false;
+
+  @override
+  final Uri uri = Uri.parse('http://127.0.0.1:1234/');
+
+  @override
+  Future<void> startDartDevelopmentService(
+    Uri observatoryUri, {
+    required Logger logger,
+    int? hostPort,
+    bool? ipv6,
+    bool? disableServiceAuthCodes,
+    bool cacheStartupProfile = false,
+  }) async {
+    started = true;
+  }
+
+  @override
+  Future<void> shutdown() async {
+    disposed = true;
+  }
+
+  @override
+  void setExternalDevToolsUri(Uri uri) {}
+}
diff --git a/packages/flutter_tools/test/general.shard/resident_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_runner_test.dart
index a7f212a7ca..30b1ad3885 100644
--- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart
+++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart
@@ -152,6 +152,10 @@ const FakeVmServiceRequest evictShader = FakeVmServiceRequest(
   }
 );
 
+const FakeVmServiceRequest serveObservatory = FakeVmServiceRequest(
+  method: '_serveObservatory',
+);
+
 final Uri testUri = Uri.parse('foo://bar');
 
 void main() {
@@ -191,6 +195,7 @@ void main() {
   testUsingContext('ResidentRunner can attach to device successfully', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
+      serveObservatory,
       listViews,
     ]);
     final Completer<DebugConnectionInfo> futureConnectionInfo = Completer<DebugConnectionInfo>.sync();
@@ -214,6 +219,7 @@ void main() {
       .createSync(recursive: true);
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
+      serveObservatory,
       listViews,
     ]);
     final FakeResidentCompiler residentCompiler = FakeResidentCompiler()
@@ -305,6 +311,7 @@ void main() {
       .createSync(recursive: true);
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
+      serveObservatory,
       listViews,
     ]);
     final FakeResidentCompiler residentCompiler = FakeResidentCompiler()
@@ -329,6 +336,7 @@ void main() {
   testUsingContext('ResidentRunner can attach to device successfully with --fast-start', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
+      serveObservatory,
       listViews,
       listViews,
       FakeVmServiceRequest(
@@ -397,6 +405,7 @@ void main() {
   testUsingContext('ResidentRunner can handle an RPC exception from hot reload', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
+      serveObservatory,
       listViews,
       listViews,
     ]);
@@ -430,6 +439,7 @@ void main() {
   testUsingContext('ResidentRunner fails its operation if the device initialization is not complete', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
+      serveObservatory,
       listViews,
     ]);
     final Completer<DebugConnectionInfo> futureConnectionInfo = Completer<DebugConnectionInfo>.sync();
@@ -451,6 +461,7 @@ void main() {
   testUsingContext('ResidentRunner can handle an reload-barred exception from hot reload', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
+      serveObservatory,
       listViews,
       listViews,
     ]);
@@ -486,6 +497,7 @@ void main() {
   testUsingContext('ResidentRunner reports hot reload event with null safety analytics', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
+      serveObservatory,
       listViews,
       listViews,
     ]);
@@ -532,7 +544,8 @@ void main() {
 
   testUsingContext('ResidentRunner does not reload sources if no sources changed', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
-     listViews,
+      listViews,
+      serveObservatory,
       listViews,
       listViews,
       FakeVmServiceRequest(
@@ -577,6 +590,7 @@ void main() {
   testUsingContext('ResidentRunner reports error with missing entrypoint file', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
+      serveObservatory,
       listViews,
       listViews,
       FakeVmServiceRequest(
@@ -637,6 +651,7 @@ void main() {
    testUsingContext('ResidentRunner resets compilation time on reload reject', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
+      serveObservatory,
       listViews,
       listViews,
       FakeVmServiceRequest(
@@ -701,6 +716,7 @@ void main() {
   testUsingContext('ResidentRunner can send target platform to analytics from hot reload', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
+      serveObservatory,
       listViews,
       listViews,
       FakeVmServiceRequest(
@@ -764,6 +780,7 @@ void main() {
   testUsingContext('ResidentRunner can perform fast reassemble', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
+      serveObservatory,
       FakeVmServiceRequest(
         method: 'getVM',
         jsonResponse: fakeVM.toJson(),
@@ -854,6 +871,7 @@ void main() {
   testUsingContext('ResidentRunner reports hot reload time details', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
+      serveObservatory,
       FakeVmServiceRequest(
         method: 'getVM',
         jsonResponse: fakeVM.toJson(),
@@ -943,6 +961,7 @@ void main() {
   testUsingContext('ResidentRunner can send target platform to analytics from full restart', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
+      serveObservatory,
       listViews,
       listViews,
       FakeVmServiceRequest(
@@ -1003,6 +1022,7 @@ void main() {
   testUsingContext('ResidentRunner can remove breakpoints and exception-pause-mode from paused isolate during hot restart', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
+      serveObservatory,
       listViews,
       listViews,
       FakeVmServiceRequest(
@@ -1076,6 +1096,7 @@ void main() {
   testUsingContext('ResidentRunner will alternative the name of the dill file uploaded for a hot restart', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
+      serveObservatory,
       listViews,
       listViews,
       FakeVmServiceRequest(
@@ -1198,6 +1219,7 @@ void main() {
   testUsingContext('ResidentRunner Can handle an RPC exception from hot restart', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
+      serveObservatory,
       listViews,
     ]);
     final Completer<DebugConnectionInfo> futureConnectionInfo = Completer<DebugConnectionInfo>.sync();
@@ -1634,6 +1656,7 @@ flutter:
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
       listViews,
+      serveObservatory,
     ]);
     residentRunner = ColdRunner(
       <FlutterDevice>[
@@ -1676,6 +1699,7 @@ flutter:
   testUsingContext('HotRunner writes vm service file when providing debugging option', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
+      serveObservatory,
       listViews,
     ], wsAddress: testUri);
     globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true);
@@ -1698,6 +1722,7 @@ flutter:
   testUsingContext('HotRunner copies compiled app.dill to cache during startup', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
+      serveObservatory,
       listViews,
     ], wsAddress: testUri);
     globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true);
@@ -1726,6 +1751,7 @@ flutter:
   testUsingContext('HotRunner copies compiled app.dill to cache during startup with dart defines', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
+      serveObservatory,
       listViews,
     ], wsAddress: testUri);
     globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true);
@@ -1756,6 +1782,7 @@ flutter:
   testUsingContext('HotRunner copies compiled app.dill to cache during startup with null safety', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
+      serveObservatory,
       listViews,
     ], wsAddress: testUri);
     globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true);
@@ -1786,6 +1813,7 @@ flutter:
   testUsingContext('HotRunner copies compiled app.dill to cache during startup with track-widget-creation', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
+      serveObservatory,
       listViews,
     ], wsAddress: testUri);
     globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true);
@@ -1809,6 +1837,7 @@ flutter:
   testUsingContext('HotRunner does not copy app.dill if a dillOutputPath is given', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
+      serveObservatory,
       listViews,
     ], wsAddress: testUri);
     globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true);
@@ -1832,6 +1861,7 @@ flutter:
   testUsingContext('HotRunner copies compiled app.dill to cache during startup with --track-widget-creation', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
+      serveObservatory,
       listViews,
     ], wsAddress: testUri);
     globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true);
@@ -1859,6 +1889,7 @@ flutter:
   testUsingContext('HotRunner calls device dispose', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
+      serveObservatory,
       listViews,
     ], wsAddress: testUri);
     globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true);
@@ -1879,6 +1910,7 @@ flutter:
   testUsingContext('HotRunner handles failure to write vmservice file', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
+      serveObservatory,
       listViews,
     ]);
     globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true);
@@ -1903,6 +1935,7 @@ flutter:
   testUsingContext('ColdRunner writes vm service file when providing debugging option', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
+      serveObservatory,
     ], wsAddress: testUri);
     globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true);
     residentRunner = ColdRunner(
diff --git a/packages/flutter_tools/test/integration.shard/flutter_attach_test.dart b/packages/flutter_tools/test/integration.shard/flutter_attach_test.dart
index a2621c4def..48bf091dd3 100644
--- a/packages/flutter_tools/test/integration.shard/flutter_attach_test.dart
+++ b/packages/flutter_tools/test/integration.shard/flutter_attach_test.dart
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:convert';
+
 import 'package:file/file.dart';
 import 'package:flutter_tools/src/base/io.dart';
 import 'package:vm_service/vm_service.dart';
@@ -157,4 +159,64 @@ void main() {
       expect(vmServiceUri.port, equals(ddsPort));
     });
   });
+
+  group('--serve-observatory', () {
+    late FlutterRunTestDriver flutterRun, flutterAttach;
+
+    setUp(() async {
+      flutterRun = FlutterRunTestDriver(tempDir,    logPrefix: '   RUN  ');
+      flutterAttach = FlutterRunTestDriver(
+        tempDir,
+        logPrefix: 'ATTACH  ',
+        // Only one DDS instance can be connected to the VM service at a time.
+        // DDS can also only initialize if the VM service doesn't have any existing
+        // clients, so we'll just let _flutterRun be responsible for spawning DDS.
+        spawnDdsInstance: false,
+      );
+    });
+
+    tearDown(() async {
+      await flutterAttach.detach();
+      await flutterRun.stop();
+    });
+
+    Future<bool> isObservatoryAvailable() async {
+      final HttpClient client = HttpClient();
+      final Uri vmServiceUri = Uri(
+        scheme: 'http',
+        host: flutterRun.vmServiceWsUri!.host,
+        port: flutterRun.vmServicePort,
+      );
+
+      final HttpClientRequest request = await client.getUrl(vmServiceUri);
+      final HttpClientResponse response = await request.close();
+      final String content = await response.transform(utf8.decoder).join();
+      return content.contains('Dart VM Observatory');
+    }
+
+    testWithoutContext('enables Observatory on run', () async {
+        await flutterRun.run(
+          withDebugger: true,
+          // TODO(bkonyi): uncomment once Observatory is disabled by default
+          // See https://github.com/dart-lang/sdk/issues/50233
+          // serveObservatory: true,
+        );
+        expect(await isObservatoryAvailable(), true);
+    });
+
+    testWithoutContext('enables Observatory on attach', () async {
+      await flutterRun.run(withDebugger: true, serveObservatory: false);
+      // Bail out if Observatory is still served by default in the VM.
+      if (await isObservatoryAvailable()) {
+        return;
+      }
+      await flutterAttach.attach(
+        flutterRun.vmServicePort!,
+        // TODO(bkonyi): uncomment once Observatory is disabled by default
+        // See https://github.com/dart-lang/sdk/issues/50233
+        // serveObservatory: true,
+      );
+      expect(await isObservatoryAvailable(), true);
+    });
+  });
 }
diff --git a/packages/flutter_tools/test/integration.shard/test_driver.dart b/packages/flutter_tools/test/integration.shard/test_driver.dart
index 4f3af30161..f233b3793e 100644
--- a/packages/flutter_tools/test/integration.shard/test_driver.dart
+++ b/packages/flutter_tools/test/integration.shard/test_driver.dart
@@ -499,6 +499,7 @@ class FlutterRunTestDriver extends FlutterTestDriver {
     bool expressionEvaluation = true,
     bool structuredErrors = false,
     bool singleWidgetReloads = false,
+    bool serveObservatory = true,
     String? script,
     List<String>? additionalCommandArgs,
   }) async {
@@ -509,6 +510,7 @@ class FlutterRunTestDriver extends FlutterTestDriver {
           '--disable-service-auth-codes',
         '--machine',
         if (!spawnDdsInstance) '--no-dds',
+        '--${serveObservatory ? '' : 'no-'}serve-observatory',
         ...getLocalEngineArguments(),
         '-d',
         if (chrome)
@@ -537,6 +539,7 @@ class FlutterRunTestDriver extends FlutterTestDriver {
     bool startPaused = false,
     bool pauseOnExceptions = false,
     bool singleWidgetReloads = false,
+    bool serveObservatory = true,
     List<String>? additionalCommandArgs,
   }) async {
     _attachPort = port;
@@ -547,6 +550,7 @@ class FlutterRunTestDriver extends FlutterTestDriver {
         '--machine',
         if (!spawnDdsInstance)
           '--no-dds',
+        '--${serveObservatory ? '' : 'no-'}serve-observatory',
         '-d',
         'flutter-tester',
         '--debug-port',
diff --git a/packages/flutter_tools/test/integration.shard/test_test.dart b/packages/flutter_tools/test/integration.shard/test_test.dart
index 3e1e99c730..03e12438a0 100644
--- a/packages/flutter_tools/test/integration.shard/test_test.dart
+++ b/packages/flutter_tools/test/integration.shard/test_test.dart
@@ -198,6 +198,31 @@ void main() {
   testWithoutContext('flutter gold skips tests where the expectations are missing', () async {
     return _testFile('flutter_gold', automatedTestsDirectory, flutterTestDirectory, exitCode: isZero);
   });
+
+  testWithoutContext('flutter test should respect --serve-observatory', () async {
+    late final Process process;
+    try {
+      process = await _runFlutterTestConcurrent('trivial', automatedTestsDirectory, flutterTestDirectory,
+        extraArguments: const <String>['--start-paused', '--serve-observatory']);
+      final Completer<Uri> completer = Completer<Uri>();
+      final RegExp vmServiceUriRegExp = RegExp(r'((http)?:\/\/)[^\s]+');
+      late final StreamSubscription<String> sub;
+      sub = process.stdout.transform(utf8.decoder).listen((String e) {
+        if (vmServiceUriRegExp.hasMatch(e)) {
+          completer.complete(Uri.parse(vmServiceUriRegExp.firstMatch(e)!.group(0)!));
+          sub.cancel();
+        }
+      });
+      final Uri vmServiceUri = await completer.future;
+      final HttpClient client = HttpClient();
+      final HttpClientRequest request = await client.getUrl(vmServiceUri);
+      final HttpClientResponse response = await request.close();
+      final String content = await response.transform(utf8.decoder).join();
+      expect(content.contains('Dart VM Observatory'), true);
+    } finally {
+      process.kill();
+    }
+  });
 }
 
 Future<void> _testFile(
@@ -328,3 +353,45 @@ Future<ProcessResult> _runFlutterTest(
     stderrEncoding: utf8,
   );
 }
+
+Future<Process> _runFlutterTestConcurrent(
+  String? testName,
+  String workingDirectory,
+  String testDirectory, {
+  List<String> extraArguments = const <String>[],
+}) async {
+
+  String testPath;
+  if (testName == null) {
+    // Test everything in the directory.
+    testPath = testDirectory;
+    final Directory directoryToTest = fileSystem.directory(testPath);
+    if (!directoryToTest.existsSync()) {
+      fail('missing test directory: $directoryToTest');
+    }
+  } else {
+    // Test just a specific test file.
+    testPath = fileSystem.path.join(testDirectory, '${testName}_test.dart');
+    final File testFile = fileSystem.file(testPath);
+    if (!testFile.existsSync()) {
+      fail('missing test file: $testFile');
+    }
+  }
+
+  final List<String> args = <String>[
+    'test',
+    '--no-color',
+    '--no-version-check',
+    '--no-pub',
+    '--reporter',
+    'compact',
+    ...extraArguments,
+    testPath,
+  ];
+
+  return Process.start(
+    flutterBin, // Uses the precompiled flutter tool for faster tests,
+    args,
+    workingDirectory: workingDirectory,
+  );
+}
-- 
2.21.0