diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index b5cb08a46dd..fc6e13bcaf7 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -437,12 +437,12 @@ class IOSDevice extends Device { final Uri? localUri = await observatoryDiscovery?.uri; timer.cancel(); if (localUri == null) { - iosDeployDebugger?.detach(); + await iosDeployDebugger?.stopAndDumpBacktrace(); return LaunchResult.failed(); } return LaunchResult.succeeded(observatoryUri: localUri); } on ProcessException catch (e) { - iosDeployDebugger?.detach(); + await iosDeployDebugger?.stopAndDumpBacktrace(); _logger.printError(e.message); return LaunchResult.failed(); } finally { diff --git a/packages/flutter_tools/lib/src/ios/ios_deploy.dart b/packages/flutter_tools/lib/src/ios/ios_deploy.dart index 8b2c044f10d..7e6e79fe7f8 100644 --- a/packages/flutter_tools/lib/src/ios/ios_deploy.dart +++ b/packages/flutter_tools/lib/src/ios/ios_deploy.dart @@ -293,6 +293,15 @@ class IOSDeployDebugger { // (lldb) Process 6152 stopped static final RegExp _lldbProcessStopped = RegExp(r'Process \d* stopped'); + // (lldb) Process 6152 detached + static final RegExp _lldbProcessDetached = RegExp(r'Process \d* detached'); + + // Send signal to stop (pause) the app. Used before a backtrace dump. + static const String _signalStop = 'process signal SIGSTOP'; + + // Print backtrace for all threads while app is stopped. + static const String _backTraceAll = 'thread backtrace all'; + /// Launch the app on the device, and attach the debugger. /// /// Returns whether or not the debugger successfully attached. @@ -330,16 +339,41 @@ class IOSDeployDebugger { } return; } - if (line.contains('PROCESS_STOPPED') || - line.contains('PROCESS_EXITED') || - _lldbProcessExit.hasMatch(line) || - _lldbProcessStopped.hasMatch(line)) { + if (line == _signalStop) { + // The app is about to be stopped. Only show in verbose mode. + _logger.printTrace(line); + return; + } + if (line == _backTraceAll) { + // The app is stopped and the backtrace for all threads will be printed. + _logger.printTrace(line); + // Even though we're not "detached", just stopped, mark as detached so the backtrace + // is only show in verbose. + _debuggerState = _IOSDeployDebuggerState.detached; + return; + } + + if (line.contains('PROCESS_STOPPED') || _lldbProcessStopped.hasMatch(line)) { + // The app has been stopped. Dump the backtrace, and detach. + _logger.printTrace(line); + _iosDeployProcess?.stdin.writeln(_backTraceAll); + detach(); + return; + } + if (line.contains('PROCESS_EXITED') || _lldbProcessExit.hasMatch(line)) { // The app exited or crashed, so exit. Continue passing debugging // messages to the log reader until it exits to capture crash dumps. _logger.printTrace(line); exit(); return; } + if (_lldbProcessDetached.hasMatch(line)) { + // The debugger has detached from the app, and there will be no more debugging messages. + // Kill the ios-deploy process. + exit(); + return; + } + if (_debuggerState != _IOSDeployDebuggerState.attached) { _logger.printTrace(line); return; @@ -395,6 +429,22 @@ class IOSDeployDebugger { return success; } + Future stopAndDumpBacktrace() async { + if (!debuggerAttached) { + return; + } + + try { + // Stop the app, which will prompt the backtrace to be printed for all threads in the stdoutSubscription handler. + _iosDeployProcess?.stdin.writeln(_signalStop); + } on SocketException catch (error) { + // Best effort, try to detach, but maybe the app already exited or already detached. + _logger.printTrace('Could not stop app from debugger: $error'); + } + // Wait for logging to finish on process exit. + return logLines.drain(); + } + void detach() { if (!debuggerAttached) { return; @@ -403,7 +453,6 @@ class IOSDeployDebugger { 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'); 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 d3c46b002a0..e9c9c2c61b5 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 @@ -90,11 +90,14 @@ void main () { logger = BufferLogger.test(); }); - testWithoutContext('debugger attached', () async { + testWithoutContext('debugger attached and stopped', () async { + final StreamController> stdin = StreamController>(); + final Stream stdinStream = stdin.stream.transform(const Utf8Decoder()); final FakeProcessManager processManager = FakeProcessManager.list([ - const FakeCommand( - command: ['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', + FakeCommand( + command: const ['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 stop\r\nthread backtrace all\r\n* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP", + stdin: IOSink(stdin.sink), ), ]); final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test( @@ -113,7 +116,15 @@ void main () { 'Log on attach2', '', '', - 'Log after process exit', + 'Log after process stop' + ]); + expect(logger.traceText, contains('PROCESS_STOPPED')); + expect(logger.traceText, contains('thread backtrace all')); + expect(logger.traceText, contains('* thread #1')); + expect(await stdinStream.take(3).toList(), [ + 'thread backtrace all', + '\n', + 'process detach', ]); }); @@ -141,11 +152,14 @@ void main () { }); testWithoutContext('app crash', () async { + final StreamController> stdin = StreamController>(); + final Stream stdinStream = stdin.stream.transform(const Utf8Decoder()); final FakeProcessManager processManager = FakeProcessManager.list([ - const FakeCommand( - command: ['ios-deploy'], + FakeCommand( + command: const ['ios-deploy'], stdout: - '(lldb) run\r\nsuccess\r\nLog on attach\r\n(lldb) Process 6156 stopped\r\n* thread #1, stop reason = Assertion failed:', + '(lldb) run\r\nsuccess\r\nLog on attach\r\n(lldb) Process 6156 stopped\r\n* thread #1, stop reason = Assertion failed:\r\nthread backtrace all\r\n* thread #1, stop reason = Assertion failed:', + stdin: IOSink(stdin.sink), ), ]); final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test( @@ -162,6 +176,14 @@ void main () { 'Log on attach', '* thread #1, stop reason = Assertion failed:', ]); + expect(logger.traceText, contains('Process 6156 stopped')); + expect(logger.traceText, contains('thread backtrace all')); + expect(logger.traceText, contains('* thread #1')); + expect(await stdinStream.take(3).toList(), [ + 'thread backtrace all', + '\n', + 'process detach', + ]); }); testWithoutContext('attach failed', () async { @@ -268,6 +290,31 @@ void main () { iosDeployDebugger.detach(); expect(await stdinStream.first, 'process detach'); }); + + testWithoutContext('stop with backtrace', () async { + final StreamController> stdin = StreamController>(); + final Stream stdinStream = stdin.stream.transform(const Utf8Decoder()); + final FakeProcessManager processManager = FakeProcessManager.list([ + FakeCommand( + command: const [ + 'ios-deploy', + ], + stdout: + '(lldb) run\nsuccess\nLog on attach\n(lldb) Process 6156 stopped\n* thread #1, stop reason = Assertion failed:\n(lldb) Process 6156 detached', + stdin: IOSink(stdin.sink), + ), + ]); + final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test( + processManager: processManager, + ); + await iosDeployDebugger.launchAndAttach(); + await iosDeployDebugger.stopAndDumpBacktrace(); + expect(await stdinStream.take(3).toList(), [ + 'thread backtrace all', + '\n', + 'process detach', + ]); + }); }); group('IOSDeploy.uninstallApp', () {