Hide error on mDNS registration failure and print warning in flutter attach (#166782)

Publishing an mDNS service stopped working on macOS 15.4 for the iOS
Simulator. `DNSServiceRegister` always returns a callback with error
code `kDNSServiceErr_PolicyDenied`, as if the user had rejected the
permissions popup (except no popup is ever shown). mDNS is used to
discover the Dart VM when running `flutter attach`.

This PR does not display the error message when `DNSServiceRegister`
fails for iOS Simulators. Instead, it prints a warning if `flutter
attach` takes too long with instructions to either use `--debug-url` or
to run `flutter attach` before running the app.

If `DNSServiceRegister` works again in a future macOS update, mDNS will
continue to work the way it did previously.

Workaround for https://github.com/flutter/flutter/issues/166333.

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterDartVMServicePublisher.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterDartVMServicePublisher.mm
index 690d8c1..ce5ac85 100644
--- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterDartVMServicePublisher.mm
+++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterDartVMServicePublisher.mm
@@ -76,8 +76,6 @@
 }
 
 - (void)publishServiceProtocolPort:(NSURL*)url {
-  // TODO(vashworth): Remove once done debugging https://github.com/flutter/flutter/issues/129836
-  FML_LOG(INFO) << "Publish Service Protocol Port";
   DNSServiceFlags flags = kDNSServiceFlagsDefault;
 #if TARGET_IPHONE_SIMULATOR
   // Simulator needs to use local loopback explicitly to work.
@@ -126,10 +124,20 @@
   if (errorCode == kDNSServiceErr_NoError) {
     FML_DLOG(INFO) << "FlutterDartVMServicePublisher is ready!";
   } else if (errorCode == kDNSServiceErr_PolicyDenied) {
+    // Local Network permissions on simulators stopped working in macOS 15.4 and will always return
+    // kDNSServiceErr_PolicyDenied. See
+    // https://github.com/flutter/flutter/issues/166333#issuecomment-2786720560.
+#if TARGET_IPHONE_SIMULATOR
+    FML_DLOG(WARNING)
+        << "Could not register as server for FlutterDartVMServicePublisher, permission "
+        << "denied. Check your 'Local Network' permissions for this app in the Privacy section of "
+        << "the system Settings.";
+#else   // TARGET_IPHONE_SIMULATOR
     FML_LOG(ERROR)
         << "Could not register as server for FlutterDartVMServicePublisher, permission "
         << "denied. Check your 'Local Network' permissions for this app in the Privacy section of "
         << "the system Settings.";
+#endif  // TARGET_IPHONE_SIMULATOR
   } else {
     FML_LOG(ERROR) << "Could not register as server for FlutterDartVMServicePublisher. Check your "
                       "network settings and relaunch the application.";
diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm
index c791cf2..6c3e893 100644
--- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm
+++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm
@@ -867,8 +867,6 @@
     FML_LOG(ERROR) << "Could not start a shell FlutterEngine with entrypoint: "
                    << entrypoint.UTF8String;
   } else {
-    // TODO(vashworth): Remove once done debugging https://github.com/flutter/flutter/issues/129836
-    FML_LOG(INFO) << "Enabled VM Service Publication: " << settings.enable_vm_service_publication;
     [self setUpShell:std::move(shell)
         withVMServicePublication:settings.enable_vm_service_publication];
     if ([FlutterEngine isProfilerEnabled]) {
diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart
index f4c87cf..05fd9b5 100644
--- a/packages/flutter_tools/lib/src/commands/attach.dart
+++ b/packages/flutter_tools/lib/src/commands/attach.dart
@@ -23,6 +23,7 @@
 import '../device_port_forwarder.dart';
 import '../device_vm_service_discovery_for_attach.dart';
 import '../ios/devices.dart';
+import '../ios/simulators.dart';
 import '../macos/macos_ipad_device.dart';
 import '../mdns_discovery.dart';
 import '../project.dart';
@@ -316,8 +317,20 @@
       final Status discoveryStatus = _logger.startSpinner(
         timeout: const Duration(seconds: 30),
         slowWarningCallback: () {
-          // On iOS we rely on mDNS to find Dart VM Service. Remind the user to allow local network permissions on the device.
-          if (_isIOSDevice(device)) {
+          // On iOS we rely on mDNS to find Dart VM Service.
+          if (device is IOSSimulator) {
+            // mDNS on simulators stopped working in macOS 15.4.
+            // See https://github.com/flutter/flutter/issues/166333.
+            return 'The Dart VM Service was not discovered after 30 seconds. '
+                'This may be due to limited mDNS support in the iOS Simulator.\n\n'
+                'Click "Allow" to the prompt on your device asking if you would like to find and connect devices on your local network. '
+                'If you selected "Don\'t Allow", you can turn it on in Settings > Your App Name > Local Network. '
+                "If you don't see your app in the Settings, uninstall the app and rerun to see the prompt again.\n\n"
+                'If you do not receive a prompt, either run "flutter attach" before starting the '
+                'app or use the Dart VM service URL from the Xcode console with '
+                '"flutter attach --debug-url=<URL>".\n';
+          } else if (_isIOSDevice(device)) {
+            // Remind the user to allow local network permissions on the device.
             return 'The Dart VM Service was not discovered after 30 seconds. This is taking much longer than expected...\n\n'
                 'Click "Allow" to the prompt on your device asking if you would like to find and connect devices on your local network. '
                 'If you selected "Don\'t Allow", you can turn it on in Settings > Your App Name > Local Network. '
@@ -326,6 +339,7 @@
 
           return 'The Dart VM Service was not discovered after 30 seconds. This is taking much longer than expected...\n';
         },
+        warningColor: TerminalColor.cyan,
       );
 
       vmServiceUri = vmServiceDiscovery.uris;
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart
index 22aed41..12e7fb5 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart
@@ -4,6 +4,7 @@
 
 import 'dart:async';
 
+import 'package:fake_async/fake_async.dart';
 import 'package:file/memory.dart';
 import 'package:flutter_tools/src/android/android_device.dart';
 import 'package:flutter_tools/src/application_package.dart';
@@ -25,6 +26,7 @@
 import 'package:flutter_tools/src/device_vm_service_discovery_for_attach.dart';
 import 'package:flutter_tools/src/ios/application_package.dart';
 import 'package:flutter_tools/src/ios/devices.dart';
+import 'package:flutter_tools/src/ios/simulators.dart';
 import 'package:flutter_tools/src/macos/macos_ipad_device.dart';
 import 'package:flutter_tools/src/mdns_discovery.dart';
 import 'package:flutter_tools/src/project.dart';
@@ -1528,6 +1530,52 @@
         DeviceManager: () => testDeviceManager,
       },
     );
+
+    group('prints warning when too slow', () {
+      late SlowWarningCallbackBufferLogger logger;
+
+      setUp(() {
+        logger = SlowWarningCallbackBufferLogger.test();
+      });
+
+      testUsingContext(
+        'to find on iOS Simulator',
+        () async {
+          final FakeIOSSimulator device = FakeIOSSimulator();
+          testDeviceManager.devices = <Device>[device];
+          FakeAsync().run((FakeAsync fakeAsync) {
+            createTestCommandRunner(
+              AttachCommand(
+                stdio: stdio,
+                logger: logger,
+                terminal: terminal,
+                signals: signals,
+                platform: platform,
+                processInfo: processInfo,
+                fileSystem: testFileSystem,
+              ),
+            ).run(<String>['attach']);
+
+            logger.expectedWarning =
+                'The Dart VM Service was not discovered after 30 seconds. '
+                'This may be due to limited mDNS support in the iOS Simulator.\n\n'
+                'Click "Allow" to the prompt on your device asking if you would like to find and connect devices on your local network. '
+                'If you selected "Don\'t Allow", you can turn it on in Settings > Your App Name > Local Network. '
+                "If you don't see your app in the Settings, uninstall the app and rerun to see the prompt again.\n\n"
+                'If you do not receive a prompt, either run "flutter attach" before starting the '
+                'app or use the Dart VM service URL from the Xcode console with '
+                '"flutter attach --debug-url=<URL>".\n';
+            fakeAsync.elapse(const Duration(seconds: 30));
+          });
+        },
+        overrides: <Type, Generator>{
+          FileSystem: () => testFileSystem,
+          ProcessManager: () => FakeProcessManager.any(),
+          Logger: () => logger,
+          DeviceManager: () => testDeviceManager,
+        },
+      );
+    });
   });
 }
 
@@ -1966,6 +2014,62 @@
   }
 }
 
+class FakeIOSSimulator extends Fake implements IOSSimulator {
+  @override
+  final String name = 'name';
+
+  @override
+  String get displayName => name;
+
+  @override
+  bool isSupported() => true;
+
+  @override
+  bool isSupportedForProject(FlutterProject flutterProject) => true;
+
+  @override
+  bool get isConnected => true;
+
+  @override
+  DeviceConnectionInterface get connectionInterface => DeviceConnectionInterface.attached;
+
+  @override
+  bool get ephemeral => true;
+
+  @override
+  Future<TargetPlatform> get targetPlatform async => TargetPlatform.ios;
+
+  @override
+  final PlatformType platformType = PlatformType.ios;
+
+  @override
+  bool get isWirelesslyConnected => false;
+
+  @override
+  DevicePortForwarder portForwarder = RecordingPortForwarder();
+
+  @override
+  VMServiceDiscoveryForAttach getVMServiceDiscoveryForAttach({
+    String? appId,
+    String? fuchsiaModule,
+    int? filterDevicePort,
+    int? expectedHostPort,
+    required bool ipv6,
+    required Logger logger,
+  }) {
+    final MdnsVMServiceDiscoveryForAttach mdnsVMServiceDiscoveryForAttach =
+        MdnsVMServiceDiscoveryForAttach(
+          device: this,
+          appId: appId,
+          deviceVmservicePort: filterDevicePort,
+          hostVmservicePort: expectedHostPort,
+          usesIpv6: ipv6,
+          useDeviceIPAsHost: isWirelesslyConnected,
+        );
+    return mdnsVMServiceDiscoveryForAttach;
+  }
+}
+
 class FakeMDnsClient extends Fake implements MDnsClient {
   FakeMDnsClient(
     this.ptrRecords,
@@ -2054,3 +2158,21 @@
   @override
   Stream<String> get keystrokes => StreamController<String>().stream;
 }
+
+class SlowWarningCallbackBufferLogger extends BufferLogger {
+  SlowWarningCallbackBufferLogger.test() : super.test();
+
+  String? expectedWarning;
+
+  @override
+  Status startSpinner({
+    VoidCallback? onFinish,
+    Duration? timeout,
+    SlowWarningCallback? slowWarningCallback,
+    TerminalColor? warningColor,
+  }) {
+    expect(slowWarningCallback, isNotNull);
+    expect(slowWarningCallback!(), expectedWarning);
+    return SilentStatus(stopwatch: Stopwatch(), onFinish: onFinish)..start();
+  }
+}