Skip to content

Commit cef6823

Browse files
authored
Dump backtrace when cannot attach to observatory (#98550)
1 parent 58ad6e1 commit cef6823

3 files changed

Lines changed: 111 additions & 15 deletions

File tree

packages/flutter_tools/lib/src/ios/devices.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -437,12 +437,12 @@ class IOSDevice extends Device {
437437
final Uri? localUri = await observatoryDiscovery?.uri;
438438
timer.cancel();
439439
if (localUri == null) {
440-
iosDeployDebugger?.detach();
440+
await iosDeployDebugger?.stopAndDumpBacktrace();
441441
return LaunchResult.failed();
442442
}
443443
return LaunchResult.succeeded(observatoryUri: localUri);
444444
} on ProcessException catch (e) {
445-
iosDeployDebugger?.detach();
445+
await iosDeployDebugger?.stopAndDumpBacktrace();
446446
_logger.printError(e.message);
447447
return LaunchResult.failed();
448448
} finally {

packages/flutter_tools/lib/src/ios/ios_deploy.dart

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,15 @@ class IOSDeployDebugger {
293293
// (lldb) Process 6152 stopped
294294
static final RegExp _lldbProcessStopped = RegExp(r'Process \d* stopped');
295295

296+
// (lldb) Process 6152 detached
297+
static final RegExp _lldbProcessDetached = RegExp(r'Process \d* detached');
298+
299+
// Send signal to stop (pause) the app. Used before a backtrace dump.
300+
static const String _signalStop = 'process signal SIGSTOP';
301+
302+
// Print backtrace for all threads while app is stopped.
303+
static const String _backTraceAll = 'thread backtrace all';
304+
296305
/// Launch the app on the device, and attach the debugger.
297306
///
298307
/// Returns whether or not the debugger successfully attached.
@@ -330,16 +339,41 @@ class IOSDeployDebugger {
330339
}
331340
return;
332341
}
333-
if (line.contains('PROCESS_STOPPED') ||
334-
line.contains('PROCESS_EXITED') ||
335-
_lldbProcessExit.hasMatch(line) ||
336-
_lldbProcessStopped.hasMatch(line)) {
342+
if (line == _signalStop) {
343+
// The app is about to be stopped. Only show in verbose mode.
344+
_logger.printTrace(line);
345+
return;
346+
}
347+
if (line == _backTraceAll) {
348+
// The app is stopped and the backtrace for all threads will be printed.
349+
_logger.printTrace(line);
350+
// Even though we're not "detached", just stopped, mark as detached so the backtrace
351+
// is only show in verbose.
352+
_debuggerState = _IOSDeployDebuggerState.detached;
353+
return;
354+
}
355+
356+
if (line.contains('PROCESS_STOPPED') || _lldbProcessStopped.hasMatch(line)) {
357+
// The app has been stopped. Dump the backtrace, and detach.
358+
_logger.printTrace(line);
359+
_iosDeployProcess?.stdin.writeln(_backTraceAll);
360+
detach();
361+
return;
362+
}
363+
if (line.contains('PROCESS_EXITED') || _lldbProcessExit.hasMatch(line)) {
337364
// The app exited or crashed, so exit. Continue passing debugging
338365
// messages to the log reader until it exits to capture crash dumps.
339366
_logger.printTrace(line);
340367
exit();
341368
return;
342369
}
370+
if (_lldbProcessDetached.hasMatch(line)) {
371+
// The debugger has detached from the app, and there will be no more debugging messages.
372+
// Kill the ios-deploy process.
373+
exit();
374+
return;
375+
}
376+
343377
if (_debuggerState != _IOSDeployDebuggerState.attached) {
344378
_logger.printTrace(line);
345379
return;
@@ -395,6 +429,22 @@ class IOSDeployDebugger {
395429
return success;
396430
}
397431

432+
Future<void> stopAndDumpBacktrace() async {
433+
if (!debuggerAttached) {
434+
return;
435+
}
436+
437+
try {
438+
// Stop the app, which will prompt the backtrace to be printed for all threads in the stdoutSubscription handler.
439+
_iosDeployProcess?.stdin.writeln(_signalStop);
440+
} on SocketException catch (error) {
441+
// Best effort, try to detach, but maybe the app already exited or already detached.
442+
_logger.printTrace('Could not stop app from debugger: $error');
443+
}
444+
// Wait for logging to finish on process exit.
445+
return logLines.drain();
446+
}
447+
398448
void detach() {
399449
if (!debuggerAttached) {
400450
return;
@@ -403,7 +453,6 @@ class IOSDeployDebugger {
403453
try {
404454
// Detach lldb from the app process.
405455
_iosDeployProcess?.stdin.writeln('process detach');
406-
_debuggerState = _IOSDeployDebuggerState.detached;
407456
} on SocketException catch (error) {
408457
// Best effort, try to detach, but maybe the app already exited or already detached.
409458
_logger.printTrace('Could not detach from debugger: $error');

packages/flutter_tools/test/general.shard/ios/ios_deploy_test.dart

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,14 @@ void main () {
9090
logger = BufferLogger.test();
9191
});
9292

93-
testWithoutContext('debugger attached', () async {
93+
testWithoutContext('debugger attached and stopped', () async {
94+
final StreamController<List<int>> stdin = StreamController<List<int>>();
95+
final Stream<String> stdinStream = stdin.stream.transform<String>(const Utf8Decoder());
9496
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
95-
const FakeCommand(
96-
command: <String>['ios-deploy'],
97-
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',
97+
FakeCommand(
98+
command: const <String>['ios-deploy'],
99+
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",
100+
stdin: IOSink(stdin.sink),
98101
),
99102
]);
100103
final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test(
@@ -113,7 +116,15 @@ void main () {
113116
'Log on attach2',
114117
'',
115118
'',
116-
'Log after process exit',
119+
'Log after process stop'
120+
]);
121+
expect(logger.traceText, contains('PROCESS_STOPPED'));
122+
expect(logger.traceText, contains('thread backtrace all'));
123+
expect(logger.traceText, contains('* thread #1'));
124+
expect(await stdinStream.take(3).toList(), <String>[
125+
'thread backtrace all',
126+
'\n',
127+
'process detach',
117128
]);
118129
});
119130

@@ -141,11 +152,14 @@ void main () {
141152
});
142153

143154
testWithoutContext('app crash', () async {
155+
final StreamController<List<int>> stdin = StreamController<List<int>>();
156+
final Stream<String> stdinStream = stdin.stream.transform<String>(const Utf8Decoder());
144157
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
145-
const FakeCommand(
146-
command: <String>['ios-deploy'],
158+
FakeCommand(
159+
command: const <String>['ios-deploy'],
147160
stdout:
148-
'(lldb) run\r\nsuccess\r\nLog on attach\r\n(lldb) Process 6156 stopped\r\n* thread #1, stop reason = Assertion failed:',
161+
'(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:',
162+
stdin: IOSink(stdin.sink),
149163
),
150164
]);
151165
final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test(
@@ -162,6 +176,14 @@ void main () {
162176
'Log on attach',
163177
'* thread #1, stop reason = Assertion failed:',
164178
]);
179+
expect(logger.traceText, contains('Process 6156 stopped'));
180+
expect(logger.traceText, contains('thread backtrace all'));
181+
expect(logger.traceText, contains('* thread #1'));
182+
expect(await stdinStream.take(3).toList(), <String>[
183+
'thread backtrace all',
184+
'\n',
185+
'process detach',
186+
]);
165187
});
166188

167189
testWithoutContext('attach failed', () async {
@@ -268,6 +290,31 @@ void main () {
268290
iosDeployDebugger.detach();
269291
expect(await stdinStream.first, 'process detach');
270292
});
293+
294+
testWithoutContext('stop with backtrace', () async {
295+
final StreamController<List<int>> stdin = StreamController<List<int>>();
296+
final Stream<String> stdinStream = stdin.stream.transform<String>(const Utf8Decoder());
297+
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
298+
FakeCommand(
299+
command: const <String>[
300+
'ios-deploy',
301+
],
302+
stdout:
303+
'(lldb) run\nsuccess\nLog on attach\n(lldb) Process 6156 stopped\n* thread #1, stop reason = Assertion failed:\n(lldb) Process 6156 detached',
304+
stdin: IOSink(stdin.sink),
305+
),
306+
]);
307+
final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test(
308+
processManager: processManager,
309+
);
310+
await iosDeployDebugger.launchAndAttach();
311+
await iosDeployDebugger.stopAndDumpBacktrace();
312+
expect(await stdinStream.take(3).toList(), <String>[
313+
'thread backtrace all',
314+
'\n',
315+
'process detach',
316+
]);
317+
});
271318
});
272319

273320
group('IOSDeploy.uninstallApp', () {

0 commit comments

Comments
 (0)