Skip to content

Commit c5b90e1

Browse files
authored
[CP-stable]Only use LLDB breakpoint in debug mode (flutter#185348)
Please fill in the form below, and a flutter domain expert will evaluate this cherry pick request. ### Issue Link: What is the link to the issue this cherry-pick is addressing? flutter#185150 ### Impact Description: What is the impact (ex. visual jank on Samsung phones, app crash, cannot ship an iOS app)? Does it impact development (ex. flutter doctor crashes when Android Studio is installed), or the shipping of production apps (the app crashes on launch). This information is for domain experts and release engineers to understand the consequences of saying yes or no to the cherry pick. Deploying to a physical iOS device in profile mode with Xcode 26 will hang. ### Changelog Description: Explain this cherry pick: * In one line that is accessible to most Flutter developers. * That describes the state prior to the fix. * That includes which platforms are impacted. See [best practices](https://github.com/flutter/flutter/blob/main/docs/releases/Hotfix-Documentation-Best-Practices.md) for examples. < Replace with changelog description here > [flutter/185150] When using profile mode on a physical iOS device, the app may fail to connect to the Dart VM. ### Workaround: Is there a workaround for this issue? Disable LLDB debuggin (`flutter config --no-enable-lldb-debugging`) or run through Xcode ### Risk: What is the risk level of this cherry-pick? ### Test Coverage: Are you confident that your fix is well-tested by automated tests? ### Validation Steps: What are the steps to validate that this fix works? Run one of these tests: flutter#185150 (comment)
1 parent cc0734a commit c5b90e1

7 files changed

Lines changed: 111 additions & 7 deletions

File tree

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import '../base/logger.dart';
1414
import '../base/process.dart';
1515
import '../base/template.dart';
1616
import '../base/utils.dart';
17+
import '../build_info.dart';
1718
import '../convert.dart';
1819
import '../device.dart';
1920
import '../macos/xcode.dart';
@@ -96,6 +97,7 @@ class IOSCoreDeviceLauncher {
9697
required String bundleId,
9798
required List<String> launchArguments,
9899
required ShutdownHooks shutdownHooks,
100+
required BuildMode mode,
99101
}) async {
100102
// Install app to device
101103
final (bool installStatus, IOSCoreDeviceInstallResult? installResult) = await _coreDeviceControl
@@ -139,6 +141,7 @@ class IOSCoreDeviceLauncher {
139141
deviceId: deviceId,
140142
appProcessId: processId,
141143
lldbLogForwarder: lldbLogForwarder,
144+
mode: mode,
142145
);
143146

144147
// If it fails to attach with lldb, kill the launched process so it doesn't stay hanging.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,6 +1057,7 @@ class IOSDevice extends Device {
10571057
bundleId: package.id,
10581058
launchArguments: launchArguments,
10591059
shutdownHooks: globals.shutdownHooks,
1060+
mode: debuggingOptions.buildInfo.mode,
10601061
);
10611062

10621063
// If it succeeds to launch with LLDB, return, otherwise continue on to

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

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import '../base/io.dart';
1111
import '../base/logger.dart';
1212
import '../base/process.dart';
1313
import '../base/utils.dart';
14+
import '../build_info.dart';
1415

1516
/// LLDB is the default debugger in Xcode on macOS. Once the application has
1617
/// launched on a physical iOS device, you can attach to it using LLDB.
@@ -42,10 +43,15 @@ class LLDB {
4243
/// Example: (lldb) Process 6152 stopped
4344
static final _lldbProcessStopped = RegExp(r'Process \d* stopped');
4445

45-
/// Pattern of lldb log when the process is resuming and the breakpoint is added.
46+
/// Pattern of lldb log when the process is resuming.
47+
///
48+
/// Example: (lldb) Process 6152 resuming
49+
static final _lldbProcessResuming = RegExp(r'Process \d+ resuming');
50+
51+
/// Pattern of lldb log when the process has started and the breakpoint is added.
4652
///
4753
/// Example: (lldb) 1 location added to breakpoint 1
48-
static final _lldbProcessResuming = RegExp(r'location added to breakpoint');
54+
static final _lldbBreakpointAdded = RegExp(r'location added to breakpoint');
4955

5056
/// Pattern of lldb log when the breakpoint is added.
5157
///
@@ -88,6 +94,7 @@ return False
8894
required String deviceId,
8995
required int appProcessId,
9096
required LLDBLogForwarder lldbLogForwarder,
97+
required BuildMode mode,
9198
}) async {
9299
Timer? timer;
93100
try {
@@ -111,9 +118,11 @@ return False
111118
return false;
112119
}
113120
await _selectDevice(deviceId);
114-
await _setBreakpoint();
121+
if (mode == BuildMode.debug) {
122+
await _setBreakpoint();
123+
}
115124
await _attachToAppProcess(appProcessId);
116-
await _resumeProcess();
125+
await _resumeProcess(mode);
117126
_isAttached = true;
118127
} on _LLDBError catch (e) {
119128
_logger.printTrace('lldb failed with error: ${e.message}');
@@ -146,7 +155,6 @@ return False
146155
appProcessId: appProcessId,
147156
logger: _logger,
148157
);
149-
150158
final StreamSubscription<String> stdoutSubscription = _lldbProcess!.stdout
151159
.transform(utf8LineDecoder)
152160
.listen((String line) {
@@ -248,9 +256,12 @@ return False
248256
}
249257

250258
/// Resume the stopped process.
251-
Future<void> _resumeProcess() async {
259+
Future<void> _resumeProcess(BuildMode mode) async {
252260
final Future<String> futureLog = _startWaitingForLog(
253-
_lldbProcessResuming,
261+
// When using debug mode, a breakpoint is added once the process resumes and no resume log
262+
// is shown. Instead we match on the breakpoint added log. In profile mode, a resume log is
263+
// shown once the process resumes and no breakpoint log is shown.
264+
mode == BuildMode.debug ? _lldbBreakpointAdded : _lldbProcessResuming,
254265
).then((value) => value, onError: _handleAsyncError);
255266

256267
await _lldbProcess?.stdinWriteln('process continue');

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ void main() {
217217
bundleId: 'bundle-id',
218218
launchArguments: <String>[],
219219
shutdownHooks: FakeShutdownHooks(),
220+
mode: BuildMode.debug,
220221
);
221222

222223
expect(result, isTrue);
@@ -267,6 +268,7 @@ void main() {
267268
bundleId: 'bundle-id',
268269
launchArguments: <String>[],
269270
shutdownHooks: FakeShutdownHooks(),
271+
mode: BuildMode.debug,
270272
);
271273

272274
expect(result, isFalse);
@@ -311,6 +313,7 @@ void main() {
311313
bundleId: 'bundle-id',
312314
launchArguments: <String>[],
313315
shutdownHooks: FakeShutdownHooks(),
316+
mode: BuildMode.debug,
314317
);
315318

316319
expect(result, isFalse);
@@ -361,6 +364,7 @@ void main() {
361364
bundleId: 'bundle-id',
362365
launchArguments: <String>[],
363366
shutdownHooks: FakeShutdownHooks(),
367+
mode: BuildMode.debug,
364368
);
365369

366370
expect(result, isFalse);
@@ -405,6 +409,7 @@ void main() {
405409
bundleId: 'bundle-id',
406410
launchArguments: <String>[],
407411
shutdownHooks: FakeShutdownHooks(),
412+
mode: BuildMode.debug,
408413
);
409414

410415
expect(result, isFalse);
@@ -451,6 +456,7 @@ void main() {
451456
bundleId: 'bundle-id',
452457
launchArguments: <String>[],
453458
shutdownHooks: FakeShutdownHooks(),
459+
mode: BuildMode.debug,
454460
);
455461

456462
expect(result, isFalse);
@@ -499,6 +505,7 @@ void main() {
499505
bundleId: 'bundle-id',
500506
launchArguments: <String>[],
501507
shutdownHooks: FakeShutdownHooks(),
508+
mode: BuildMode.debug,
502509
);
503510

504511
expect(result, isFalse);
@@ -3958,6 +3965,7 @@ class FakeLLDB extends Fake implements LLDB {
39583965
required String deviceId,
39593966
required int appProcessId,
39603967
required LLDBLogForwarder lldbLogForwarder,
3968+
required BuildMode mode,
39613969
}) async {
39623970
attemptedToAttach = true;
39633971
return attachSuccess;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1716,6 +1716,7 @@ class FakeIOSCoreDeviceLauncher extends Fake implements IOSCoreDeviceLauncher {
17161716
required String bundlePath,
17171717
required String bundleId,
17181718
required List<String> launchArguments,
1719+
required BuildMode mode,
17191720
required ShutdownHooks shutdownHooks,
17201721
}) async {
17211722
return true;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1803,6 +1803,7 @@ class FakeIOSCoreDeviceLauncher extends Fake implements IOSCoreDeviceLauncher {
18031803
required String bundlePath,
18041804
required String bundleId,
18051805
required List<String> launchArguments,
1806+
required BuildMode mode,
18061807
required ShutdownHooks shutdownHooks,
18071808
}) async {
18081809
launchedWithLLDB = true;

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

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'package:fake_async/fake_async.dart';
1010
import 'package:flutter_tools/src/base/io.dart';
1111
import 'package:flutter_tools/src/base/logger.dart';
1212
import 'package:flutter_tools/src/base/process.dart';
13+
import 'package:flutter_tools/src/build_info.dart';
1314
import 'package:flutter_tools/src/ios/lldb.dart';
1415
import 'package:test/fake.dart';
1516

@@ -42,6 +43,7 @@ void main() {
4243
deviceId: deviceId,
4344
appProcessId: appProcessId,
4445
lldbLogForwarder: FakeLLDBLogForwarder(),
46+
mode: BuildMode.debug,
4547
);
4648
expect(success, isFalse);
4749
expect(lldb.isRunning, isFalse);
@@ -127,6 +129,78 @@ Target 0: (Runner) stopped.
127129
deviceId: deviceId,
128130
appProcessId: appProcessId,
129131
lldbLogForwarder: FakeLLDBLogForwarder(),
132+
mode: BuildMode.debug,
133+
);
134+
expect(success, isTrue);
135+
expect(lldb.isRunning, isTrue);
136+
expect(lldb.appProcessId, appProcessId);
137+
expect(expectedInputs, isEmpty);
138+
expect(processManager.hasRemainingExpectations, isFalse);
139+
expect(logger.errorText, isEmpty);
140+
});
141+
142+
testWithoutContext('attachAndStart returns true on success for profile mode', () async {
143+
const deviceId = '123';
144+
const appProcessId = 5678;
145+
146+
final processAttachCompleter = Completer<List<int>>();
147+
final processResumedCompleted = Completer<List<int>>();
148+
149+
final stdoutStream = Stream<List<int>>.fromFutures([
150+
processAttachCompleter.future,
151+
processResumedCompleted.future,
152+
]);
153+
154+
final stdinController = StreamController<List<int>>();
155+
156+
final processCompleter = Completer<void>();
157+
final lldbCommand = FakeLLDBCommand(
158+
command: const <String>['lldb'],
159+
completer: processCompleter,
160+
stdin: io.IOSink(stdinController.sink),
161+
stdout: stdoutStream,
162+
stderr: const Stream.empty(),
163+
);
164+
165+
final logger = BufferLogger.test();
166+
167+
final processManager = FakeLLDBProcessManager([lldbCommand]);
168+
final processUtils = ProcessUtils(processManager: processManager, logger: logger);
169+
final lldb = LLDB(logger: logger, processUtils: processUtils);
170+
171+
const processAttachMatcher = 'device process attach --pid $appProcessId';
172+
const processResumedMatcher = 'process continue';
173+
final expectedInputs = ['device select $deviceId', processAttachMatcher, processResumedMatcher];
174+
175+
stdinController.stream.transform<String>(utf8.decoder).transform(const LineSplitter()).listen((
176+
String line,
177+
) {
178+
expectedInputs.remove(line);
179+
if (line == processAttachMatcher) {
180+
processAttachCompleter.complete(
181+
utf8.encode('''
182+
Process 568 stopped
183+
* thread #1, stop reason = signal SIGSTOP
184+
frame #0: 0x0000000102c7b240 dyld`_dyld_start
185+
dyld`_dyld_start:
186+
-> 0x102c7b240 <+0>: mov x0, sp
187+
0x102c7b244 <+4>: and sp, x0, #0xfffffffffffffff0
188+
0x102c7b248 <+8>: mov x29, #0x0 ; =0
189+
0x102c7b24c <+12>: mov x30, #0x0 ; =0
190+
Target 0: (Runner) stopped.
191+
'''),
192+
);
193+
}
194+
if (line == processResumedMatcher) {
195+
processResumedCompleted.complete(utf8.encode('Process 568 resuming\n'));
196+
}
197+
});
198+
199+
final bool success = await lldb.attachAndStart(
200+
deviceId: deviceId,
201+
appProcessId: appProcessId,
202+
lldbLogForwarder: FakeLLDBLogForwarder(),
203+
mode: BuildMode.profile,
130204
);
131205
expect(success, isTrue);
132206
expect(lldb.isRunning, isTrue);
@@ -181,6 +255,7 @@ Target 0: (Runner) stopped.
181255
deviceId: deviceId,
182256
appProcessId: appProcessId,
183257
lldbLogForwarder: FakeLLDBLogForwarder(),
258+
mode: BuildMode.debug,
184259
);
185260
expect(success, isFalse);
186261
expect(lldb.isRunning, isFalse);
@@ -231,6 +306,7 @@ Target 0: (Runner) stopped.
231306
deviceId: deviceId,
232307
appProcessId: appProcessId,
233308
lldbLogForwarder: FakeLLDBLogForwarder(),
309+
mode: BuildMode.debug,
234310
);
235311
expect(success, isFalse);
236312
expect(lldb.isRunning, isFalse);
@@ -276,6 +352,7 @@ Target 0: (Runner) stopped.
276352
deviceId: deviceId,
277353
appProcessId: appProcessId,
278354
lldbLogForwarder: FakeLLDBLogForwarder(),
355+
mode: BuildMode.debug,
279356
);
280357
time.elapse(const Duration(minutes: 2));
281358
time.flushMicrotasks();
@@ -371,6 +448,7 @@ Target 0: (Runner) stopped.
371448
deviceId: deviceId,
372449
appProcessId: appProcessId,
373450
lldbLogForwarder: lldbLogForwarder,
451+
mode: BuildMode.debug,
374452
);
375453

376454
logAfterAttachCompleter.complete(utf8.encode('$ignoreLog\n$expectedForwardedLog\n'));
@@ -422,6 +500,7 @@ Target 0: (Runner) stopped.
422500
deviceId: deviceId,
423501
appProcessId: appProcessId,
424502
lldbLogForwarder: FakeLLDBLogForwarder(),
503+
mode: BuildMode.debug,
425504
),
426505
);
427506

0 commit comments

Comments
 (0)