Noninteractive iOS debugger session (#68145)

diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart
index d67a70b..867b25b 100644
--- a/packages/flutter_tools/lib/src/ios/devices.dart
+++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -468,12 +468,10 @@
         packageName: FlutterProject.current().manifest.appName,
       );
       if (localUri == null) {
-        iosDeployDebugger?.detach();
         return LaunchResult.failed();
       }
       return LaunchResult.succeeded(observatoryUri: localUri);
     } on ProcessException catch (e) {
-      iosDeployDebugger?.detach();
       _logger.printError(e.message);
       return LaunchResult.failed();
     } finally {
@@ -486,12 +484,7 @@
     IOSApp app, {
     String userIdentifier,
   }) async {
-    // If the debugger is not attached, killing the ios-deploy process won't stop the app.
-    if (iosDeployDebugger!= null && iosDeployDebugger.debuggerAttached) {
-      // Avoid null.
-      return iosDeployDebugger?.exit() == true;
-    }
-    return false;
+    return iosDeployDebugger?.exit();
   }
 
   @override
@@ -723,8 +716,8 @@
     }
 
     void logMessage(vm_service.Event event) {
-      if (_iosDeployDebugger != null && _iosDeployDebugger.debuggerAttached) {
-        // Prefer the more complete logs from the  attached debugger.
+      if (_iosDeployDebugger != null) {
+        // Prefer the more complete logs from the debugger.
         return;
       }
       final String message = processVmServiceMessage(event);
@@ -739,7 +732,7 @@
     ]);
   }
 
-  /// Log reader will listen to [debugger.logLines] and will detach debugger on dispose.
+  /// Log reader will listen to [debugger.logLines].
   set debuggerStream(IOSDeployDebugger debugger) {
     // Logging is gathered from syslog on iOS 13 and earlier.
     if (_majorSdkVersion < minimumUniversalLoggingSdkVersion) {
@@ -818,7 +811,6 @@
       loggingSubscription.cancel();
     }
     _idevicesyslogProcess?.kill();
-    _iosDeployDebugger?.detach();
   }
 }
 
diff --git a/packages/flutter_tools/lib/src/ios/ios_deploy.dart b/packages/flutter_tools/lib/src/ios/ios_deploy.dart
index 84186ec..756e4ec 100644
--- a/packages/flutter_tools/lib/src/ios/ios_deploy.dart
+++ b/packages/flutter_tools/lib/src/ios/ios_deploy.dart
@@ -118,14 +118,13 @@
   /// Returns [IOSDeployDebugger] wrapping attached debugger logic.
   ///
   /// This method does not install the app. Call [IOSDeployDebugger.launchAndAttach()]
-  /// to install and attach the debugger to the specified app bundle.
+  /// to install the specified app bundle.
   IOSDeployDebugger prepareDebuggerForLaunch({
     @required String deviceId,
     @required String bundlePath,
     @required List<String> launchArguments,
     @required IOSDeviceInterface interfaceType,
   }) {
-    // Interactive debug session to support sending the lldb detach command.
     final List<String> launchCommand = <String>[
       'script',
       '-t',
@@ -137,6 +136,7 @@
       '--bundle',
       bundlePath,
       '--debug',
+      '--noninteractive',
       if (interfaceType != IOSDeviceInterface.network)
         '--no-wifi',
       if (launchArguments.isNotEmpty) ...<String>[
@@ -224,7 +224,7 @@
   attached,
 }
 
-/// Wrapper to launch app and attach the debugger with ios-deploy.
+/// Wrapper to launch app with the debugger with ios-deploy.
 class IOSDeployDebugger {
   IOSDeployDebugger({
     @required Logger logger,
@@ -264,15 +264,16 @@
   Stream<String> get logLines => _debuggerOutput.stream;
   final StreamController<String> _debuggerOutput = StreamController<String>.broadcast();
 
-  bool get debuggerAttached => _debuggerState == _IOSDeployDebuggerState.attached;
   _IOSDeployDebuggerState _debuggerState;
-
   // (lldb)     run
   // https://github.com/ios-control/ios-deploy/blob/1.11.2-beta.1/src/ios-deploy/ios-deploy.m#L51
   static final RegExp _lldbRun = RegExp(r'\(lldb\)\s*run');
 
-  // (lldb)     run
-  // https://github.com/ios-control/ios-deploy/blob/1.11.2-beta.1/src/ios-deploy/ios-deploy.m#L51
+  // (lldb)     autoexit
+  // hhttps://github.com/ios-control/ios-deploy/blob/1.11.2-beta.1/src/ios-deploy/ios-deploy.m#L61
+  static final RegExp _lldbAutoexit = RegExp(r'\(lldb\)\s*autoexit');
+
+  // From lldb on exit.
   static final RegExp _lldbProcessExit = RegExp(r'Process \d* exited with status =');
 
   /// Launch the app on the device, and attach the debugger.
@@ -295,6 +296,7 @@
 
         // (lldb)     run
         // success
+        // (lldb)     autoexit
         // 2020-09-15 13:42:25.185474-0700 Runner[477:181141] flutter: Observatory listening on http://127.0.0.1:57782/
         if (_lldbRun.hasMatch(line)) {
           _logger.printTrace(line);
@@ -324,7 +326,7 @@
           }
           return;
         }
-        if (_debuggerState != _IOSDeployDebuggerState.attached) {
+        if (_debuggerState != _IOSDeployDebuggerState.attached || _lldbAutoexit.hasMatch(line)) {
           _logger.printTrace(line);
           return;
         }
@@ -378,21 +380,6 @@
     _iosDeployProcess = null;
     return success;
   }
-
-  void detach() {
-    if (!debuggerAttached) {
-      return;
-    }
-
-    try {
-      // Detach lldb from the app process.
-      _iosDeployProcess?.stdin?.writeln('process detach');
-      _debuggerState = _IOSDeployDebuggerState.detached;
-    } on SocketException catch (error) {
-      // Best effort, try to detach, but maybe the app already exited or already detached.
-      _logger.printTrace('Could not detach from debugger: $error');
-    }
-  }
 }
 
 // Maps stdout line stream. Must return original line.
diff --git a/packages/flutter_tools/test/general.shard/ios/ios_deploy_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_deploy_test.dart
index 6ec084c..35089ac 100644
--- a/packages/flutter_tools/test/general.shard/ios/ios_deploy_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/ios_deploy_test.dart
@@ -3,10 +3,8 @@
 // found in the LICENSE file.
 
 import 'dart:async';
-import 'dart:convert';
 
 import 'package:flutter_tools/src/artifacts.dart';
-import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/logger.dart';
 import 'package:flutter_tools/src/base/platform.dart';
 import 'package:flutter_tools/src/build_info.dart';
@@ -50,6 +48,7 @@
             '--bundle',
             '/',
             '--debug',
+            '--noninteractive',
             '--args',
             <String>[
               '--enable-dart-profiling',
@@ -87,7 +86,7 @@
         final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
           const FakeCommand(
             command: <String>['ios-deploy'],
-            stdout: '(lldb)     run\r\nsuccess\r\nsuccess\r\nLog on attach1\r\n\r\nLog on attach2\r\n\r\n\r\n\r\nPROCESS_STOPPED\r\nLog after process exit',
+            stdout: '(lldb)     run\r\nsuccess\r\n(lldb)     autoexit\r\nsuccess\r\nLog on attach1\r\n\r\nLog on attach2\r\n\r\n\r\n\r\nPROCESS_STOPPED\r\nLog after process exit',
           ),
         ]);
         final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test(
@@ -111,7 +110,7 @@
         final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
           const FakeCommand(
             command: <String>['ios-deploy'],
-            stdout: '(lldb)     run\r\nsuccess\r\nLog on attach\r\nProcess 100 exited with status = 0\r\nLog after process exit',
+            stdout: '(lldb)     run\r\nsuccess\r\n(lldb)     autoexit\r\nLog on attach\r\nProcess 100 exited with status = 0\r\nLog after process exit',
           ),
         ]);
         final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test(
@@ -226,26 +225,6 @@
         expect(logger.errorText, contains('Could not attach the debugger'));
       });
     });
-
-    testWithoutContext('detach', () async {
-      final StreamController<List<int>> stdin = StreamController<List<int>>();
-      final Stream<String> stdinStream = stdin.stream.transform<String>(const Utf8Decoder());
-      final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
-        FakeCommand(
-          command: const <String>[
-            'ios-deploy',
-          ],
-          stdout: '(lldb)     run\nsuccess',
-          stdin: IOSink(stdin.sink),
-        ),
-      ]);
-      final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test(
-        processManager: processManager,
-      );
-      await iosDeployDebugger.launchAndAttach();
-      iosDeployDebugger.detach();
-      expect(await stdinStream.first, 'process detach');
-    });
   });
 
   group('IOSDeploy.uninstallApp', () {
diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_logger_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_logger_test.dart
index f866c85..ebb666b 100644
--- a/packages/flutter_tools/test/general.shard/ios/ios_device_logger_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/ios_device_logger_test.dart
@@ -239,7 +239,6 @@
       ));
 
       final MockIOSDeployDebugger iosDeployDebugger = MockIOSDeployDebugger();
-      when(iosDeployDebugger.debuggerAttached).thenReturn(true);
 
       final Stream<String> debuggingLogs = Stream<String>.fromIterable(<String>[
         'Message from debugger'
@@ -304,24 +303,6 @@
 
       await streamComplete.future;
     });
-
-    testWithoutContext('detaches debugger', () async {
-      final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
-        iMobileDevice: IMobileDevice(
-          artifacts: artifacts,
-          processManager: processManager,
-          cache: fakeCache,
-          logger: logger,
-        ),
-        useSyslog: false,
-      );
-      final MockIOSDeployDebugger iosDeployDebugger = MockIOSDeployDebugger();
-      when(iosDeployDebugger.logLines).thenAnswer((Invocation invocation) => const Stream<String>.empty());
-      logReader.debuggerStream = iosDeployDebugger;
-
-      logReader.dispose();
-      verify(iosDeployDebugger.detach());
-    });
   });
 }
 
diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart
index 2533d50..c5a2790 100644
--- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart
@@ -92,6 +92,7 @@
   '--bundle',
   '/',
   '--debug',
+  '--noninteractive',
   '--no-wifi',
   '--args',
   '--enable-dart-profiling --enable-service-port-fallback --disable-service-auth-codes --observatory-port=60700 --enable-checked-mode --verify-entry-points'
@@ -99,7 +100,7 @@
   'PATH': '/usr/bin:null',
   'DYLD_LIBRARY_PATH': '/path/to/libraries',
 },
-stdout: '(lldb)     run\nsuccess',
+stdout: '(lldb)     run\nsuccess\n(lldb)     autoexit',
 );
 
 void main() {
@@ -166,7 +167,6 @@
     expect(launchResult.started, true);
     expect(launchResult.hasObservatory, true);
     verify(globals.flutterUsage.sendEvent('ios-handshake', 'log-success')).called(1);
-    expect(await device.stopApp(iosApp), false);
   }, overrides: <Type, Generator>{
     Usage: () => MockUsage(),
   });
@@ -216,7 +216,6 @@
     expect(launchResult.started, true);
     expect(launchResult.hasObservatory, true);
     verify(globals.flutterUsage.sendEvent('ios-handshake', 'log-success')).called(1);
-    expect(await device.stopApp(iosApp), false);
   }, overrides: <Type, Generator>{
     Usage: () => MockUsage(),
   });
@@ -302,7 +301,6 @@
 
     expect(launchResult.started, true);
     expect(launchResult.hasObservatory, false);
-    expect(await device.stopApp(iosApp), false);
     expect(processManager.hasRemainingExpectations, false);
   }, overrides: <Type, Generator>{
     Usage: () => MockUsage(),
@@ -325,6 +323,7 @@
           '--bundle',
           '/',
           '--debug',
+          '--noninteractive',
           '--no-wifi',
           // The arguments below are determined by what is passed into
           // the debugging options argument to startApp.
@@ -406,67 +405,10 @@
     );
 
     expect(launchResult.started, true);
-    expect(await device.stopApp(iosApp), false);
     expect(processManager.hasRemainingExpectations, false);
   }, overrides: <Type, Generator>{
     Usage: () => MockUsage(),
   });
-
-  // Still uses context for analytics.
-  testUsingContext(
-      'IOSDevice.startApp detaches lldb when VM service connection fails',
-      () async {
-    final FileSystem fileSystem = MemoryFileSystem.test();
-
-    final MockIOSDeploy mockIOSDeploy = MockIOSDeploy();
-    final MockIOSDeployDebugger mockIOSDeployDebugger = MockIOSDeployDebugger();
-    when(mockIOSDeploy.prepareDebuggerForLaunch(
-            deviceId: anyNamed('deviceId'),
-            bundlePath: anyNamed('bundlePath'),
-            launchArguments: anyNamed('launchArguments'),
-            interfaceType: anyNamed('interfaceType')))
-        .thenReturn(mockIOSDeployDebugger);
-    when(mockIOSDeploy.installApp(
-            deviceId: anyNamed('deviceId'),
-            bundlePath: anyNamed('bundlePath'),
-            launchArguments: anyNamed('launchArguments'),
-            interfaceType: anyNamed('interfaceType')))
-        .thenAnswer((_) async => 0);
-
-    when(mockIOSDeployDebugger.launchAndAttach()).thenAnswer((_) async => true);
-
-    final IOSDevice device = setUpIOSDevice(
-      fileSystem: fileSystem,
-      iosDeploy: mockIOSDeploy,
-      vmServiceConnector: (String string, {Log log}) async {
-        throw const io.SocketException(
-          'OS Error: Connection refused, errno = 61, address = localhost, port '
-          '= 58943',
-        );
-      },
-    );
-    final IOSApp iosApp = PrebuiltIOSApp(
-      projectBundleId: 'app',
-      bundleName: 'Runner',
-      bundleDir: fileSystem.currentDirectory,
-    );
-    device.portForwarder = const NoOpDevicePortForwarder();
-    device.setLogReader(iosApp, FakeDeviceLogReader());
-
-    final LaunchResult launchResult = await device.startApp(
-      iosApp,
-      prebuiltApplication: true,
-      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
-      platformArgs: <String, dynamic>{},
-      fallbackPollingDelay: Duration.zero,
-      fallbackThrottleTimeout: const Duration(milliseconds: 10),
-    );
-
-    expect(launchResult.started, false);
-    verify(mockIOSDeployDebugger.detach()).called(1);
-  }, overrides: <Type, Generator>{
-    Usage: () => MockUsage(),
-  });
 }
 
 IOSDevice setUpIOSDevice({