diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index 379fcb5f22..a15ffe0f86 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -523,6 +523,7 @@ class IOSDevice extends Device { _logger.printError('Try launching Xcode and selecting "Product > Run" to fix the problem:'); _logger.printError(' open ios/Runner.xcworkspace'); _logger.printError(''); + await dispose(); return LaunchResult.failed(); } @@ -557,6 +558,7 @@ class IOSDevice extends Device { final Uri? serviceURL = await vmServiceDiscovery?.uri; if (serviceURL == null) { await iosDeployDebugger?.stopAndDumpBacktrace(); + await dispose(); return LaunchResult.failed(); } @@ -587,12 +589,14 @@ class IOSDevice extends Device { timer.cancel(); if (localUri == null) { await iosDeployDebugger?.stopAndDumpBacktrace(); + await dispose(); return LaunchResult.failed(); } return LaunchResult.succeeded(vmServiceUri: localUri); } on ProcessException catch (e) { await iosDeployDebugger?.stopAndDumpBacktrace(); _logger.printError(e.message); + await dispose(); return LaunchResult.failed(); } finally { startAppStatus.stop(); diff --git a/packages/flutter_tools/lib/src/ios/ios_deploy.dart b/packages/flutter_tools/lib/src/ios/ios_deploy.dart index 6461dd7abc..2d1f7d6ffb 100644 --- a/packages/flutter_tools/lib/src/ios/ios_deploy.dart +++ b/packages/flutter_tools/lib/src/ios/ios_deploy.dart @@ -314,6 +314,7 @@ class IOSDeployDebugger { // Send signal to stop (pause) the app. Used before a backtrace dump. static const String _signalStop = 'process signal SIGSTOP'; + static const String _signalStopError = 'Failed to send signal 17'; static const String _processResume = 'process continue'; static const String _processInterrupt = 'process interrupt'; @@ -401,11 +402,23 @@ class IOSDeployDebugger { } return; } - if (line == _signalStop) { + + // (lldb) process signal SIGSTOP + // or + // process signal SIGSTOP + if (line.contains(_signalStop)) { // The app is about to be stopped. Only show in verbose mode. _logger.printTrace(line); return; } + + // error: Failed to send signal 17: failed to send signal 17 + if (line.contains(_signalStopError)) { + // The stop signal failed, force exit. + exit(); + return; + } + if (line == _backTraceAll) { // The app is stopped and the backtrace for all threads will be printed. _logger.printTrace(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 57e6c3d46c..72e453823f 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 @@ -11,6 +11,7 @@ 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/base/process.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/ios/ios_deploy.dart'; @@ -147,6 +148,28 @@ void main () { expect(logger.traceText, contains('* thread #1')); }); + testWithoutContext('debugger attached and stop failed', () async { + final StreamController> stdin = StreamController>(); + final FakeProcessManager processManager = FakeProcessManager.list([ + FakeCommand( + command: const ['ios-deploy'], + stdout: '(lldb) run\r\nsuccess\r\nsuccess\r\nprocess signal SIGSTOP\r\n\r\nerror: Failed to send signal 17: failed to send signal 17', + stdin: IOSink(stdin.sink), + ), + ]); + final IOSDeployDebuggerWaitForExit iosDeployDebugger = IOSDeployDebuggerWaitForExit.test( + processManager: processManager, + logger: logger, + ); + + expect(iosDeployDebugger.logLines, emitsInOrder([ + 'success', + ])); + + expect(await iosDeployDebugger.launchAndAttach(), isTrue); + await iosDeployDebugger.exitCompleter.future; + }); + testWithoutContext('handle processing logging after process exit', () async { final StreamController> stdin = StreamController>(); // Make sure we don't hit a race where logging processed after the process exits @@ -591,3 +614,37 @@ IOSDeploy setUpIOSDeploy(ProcessManager processManager, { cache: cache, ); } + +class IOSDeployDebuggerWaitForExit extends IOSDeployDebugger { + IOSDeployDebuggerWaitForExit({ + required super.logger, + required super.processUtils, + required super.launchCommand, + required super.iosDeployEnv + }); + + /// Create a [IOSDeployDebugger] for testing. + /// + /// Sets the command to "ios-deploy" and environment to an empty map. + factory IOSDeployDebuggerWaitForExit.test({ + required ProcessManager processManager, + Logger? logger, + }) { + final Logger debugLogger = logger ?? BufferLogger.test(); + return IOSDeployDebuggerWaitForExit( + logger: debugLogger, + processUtils: ProcessUtils(logger: debugLogger, processManager: processManager), + launchCommand: ['ios-deploy'], + iosDeployEnv: {}, + ); + } + + final Completer exitCompleter = Completer(); + + @override + bool exit() { + final bool status = super.exit(); + exitCompleter.complete(); + return status; + } +} 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 c3e7116c5f..ff2572918f 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 @@ -67,6 +67,7 @@ const FakeCommand kLaunchDebugCommand = FakeCommand(command: [ // The command used to actually launch the app and attach the debugger with args in debug. FakeCommand attachDebuggerCommand({ IOSink? stdin, + String stdout = '(lldb) run\nsuccess', Completer? completer, bool isWirelessDevice = false, }) { @@ -94,7 +95,7 @@ FakeCommand attachDebuggerCommand({ 'PATH': '/usr/bin:null', 'DYLD_LIBRARY_PATH': '/path/to/libraries', }, - stdout: '(lldb) run\nsuccess', + stdout: stdout, stdin: stdin, ); } @@ -156,6 +157,54 @@ void main() { expect(await device.stopApp(iosApp), false); }); + testWithoutContext('IOSDevice.startApp twice in a row where ios-deploy fails the first time', () async { + final BufferLogger logger = BufferLogger.test(); + final FileSystem fileSystem = MemoryFileSystem.test(); + final Completer completer = Completer(); + final FakeProcessManager processManager = FakeProcessManager.list([ + attachDebuggerCommand( + stdout: 'PROCESS_EXITED', + ), + attachDebuggerCommand( + stdout: '(lldb) run\nsuccess\nThe Dart VM service is listening on http://127.0.0.1:456', + completer: completer, + ), + ]); + final IOSDevice device = setUpIOSDevice( + processManager: processManager, + fileSystem: fileSystem, + logger: logger, + ); + final IOSApp iosApp = PrebuiltIOSApp( + projectBundleId: 'app', + bundleName: 'Runner', + uncompressedBundle: fileSystem.currentDirectory, + applicationPackage: fileSystem.currentDirectory, + ); + + device.portForwarder = const NoOpDevicePortForwarder(); + + final LaunchResult launchResult = await device.startApp(iosApp, + prebuiltApplication: true, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + platformArgs: {}, + ); + + expect(launchResult.started, false); + expect(launchResult.hasVmService, false); + + final LaunchResult secondLaunchResult = await device.startApp(iosApp, + prebuiltApplication: true, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + platformArgs: {}, + discoveryTimeout: Duration.zero, + ); + completer.complete(); + expect(secondLaunchResult.started, true); + expect(secondLaunchResult.hasVmService, true); + expect(await device.stopApp(iosApp), false); + }); + testWithoutContext('IOSDevice.startApp launches in debug mode via log reading on [