Skip to content

Commit b34cf13

Browse files
vashworthkorca0220
authored andcommitted
Stream logs from devicectl and lldb (flutter#173724)
When debugging with `devicectl` and `lldb` (exclusive to Xcode 26+), stream logs from both processes instead of using `idevicesyslog`, which does not work with Xcode 26. Fixes flutter#173365. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
1 parent ff5b47a commit b34cf13

File tree

10 files changed

+1549
-108
lines changed

10 files changed

+1549
-108
lines changed

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

Lines changed: 344 additions & 43 deletions
Large diffs are not rendered by default.

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

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
/// @docImport '../resident_runner.dart';
6+
library;
7+
58
import 'dart:async';
69

710
import 'package:meta/meta.dart';
@@ -28,6 +31,7 @@ import '../device_vm_service_discovery_for_attach.dart';
2831
import '../features.dart';
2932
import '../globals.dart' as globals;
3033
import '../macos/xcdevice.dart';
34+
import '../macos/xcode.dart';
3135
import '../mdns_discovery.dart';
3236
import '../project.dart';
3337
import '../protocol_discovery.dart';
@@ -37,6 +41,7 @@ import 'core_devices.dart';
3741
import 'ios_deploy.dart';
3842
import 'ios_workflow.dart';
3943
import 'iproxy.dart';
44+
import 'lldb.dart';
4045
import 'mac.dart';
4146
import 'xcode_build_settings.dart';
4247
import 'xcode_debug.dart';
@@ -414,8 +419,11 @@ class IOSDevice extends Device {
414419
int installationResult;
415420
try {
416421
if (isCoreDevice) {
417-
installationResult =
418-
await _coreDeviceControl.installApp(deviceId: id, bundlePath: bundle.path) ? 0 : 1;
422+
final (bool installSuccess, _) = await _coreDeviceControl.installApp(
423+
deviceId: id,
424+
bundlePath: bundle.path,
425+
);
426+
installationResult = installSuccess ? 0 : 1;
419427
} else {
420428
installationResult = await _iosDeploy.installApp(
421429
deviceId: id,
@@ -964,7 +972,7 @@ class IOSDevice extends Device {
964972
// Release mode
965973

966974
// Install app to device
967-
final bool installSuccess = await _coreDeviceControl.installApp(
975+
final (bool installSuccess, _) = await _coreDeviceControl.installApp(
968976
deviceId: id,
969977
bundlePath: package.deviceBundlePath,
970978
);
@@ -991,6 +999,14 @@ class IOSDevice extends Device {
991999
final Version? xcodeVersion = globals.xcode?.currentVersion;
9921000
final bool lldbFeatureEnabled = featureFlags.isLLDBDebuggingEnabled;
9931001
if (xcodeVersion != null && xcodeVersion.major >= 26 && lldbFeatureEnabled) {
1002+
final DeviceLogReader deviceLogReader = getLogReader(
1003+
app: package,
1004+
usingCISystem: debuggingOptions.usingCISystem,
1005+
);
1006+
if (deviceLogReader is IOSDeviceLogReader) {
1007+
await deviceLogReader.listenToCoreDeviceLauncher(_coreDeviceLauncher);
1008+
}
1009+
9941010
final bool launchSuccess = await _coreDeviceLauncher.launchAppWithLLDBDebugger(
9951011
deviceId: id,
9961012
bundlePath: package.deviceBundlePath,
@@ -1138,6 +1154,7 @@ class IOSDevice extends Device {
11381154
app: app,
11391155
iMobileDevice: _iMobileDevice,
11401156
usingCISystem: usingCISystem,
1157+
xcode: globals.xcode,
11411158
),
11421159
);
11431160
}
@@ -1291,8 +1308,21 @@ String decodeSyslog(String line) {
12911308
}
12921309
}
12931310

1311+
/// Listens to multiple logging sources to get the logs from the physical iOS device.
1312+
///
1313+
/// Potential logging sources include:
1314+
/// * `idevicesyslog`
1315+
/// * `ios-deploy`
1316+
/// * `devicectl` and `lldb`
1317+
/// * Unified Logging (Dart VM)
1318+
///
1319+
/// Not all logging sources work on all devices. See [logSources] for limitations.
1320+
///
1321+
/// Logs are added to the [linesController] and consumed through the [logLines] stream by
1322+
/// [FlutterDevice.startEchoingDeviceLog].
12941323
class IOSDeviceLogReader extends DeviceLogReader {
12951324
IOSDeviceLogReader._(
1325+
this._xcode,
12961326
this._iMobileDevice,
12971327
this._majorSdkVersion,
12981328
this._deviceId,
@@ -1313,10 +1343,12 @@ class IOSDeviceLogReader extends DeviceLogReader {
13131343
required IOSDevice device,
13141344
IOSApp? app,
13151345
required IMobileDevice iMobileDevice,
1346+
required Xcode? xcode,
13161347
bool usingCISystem = false,
13171348
}) {
13181349
final String appName = app?.name?.replaceAll('.app', '') ?? '';
13191350
return IOSDeviceLogReader._(
1351+
xcode,
13201352
iMobileDevice,
13211353
device.majorSdkVersion,
13221354
device.id,
@@ -1331,6 +1363,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
13311363
/// Create an [IOSDeviceLogReader] for testing.
13321364
factory IOSDeviceLogReader.test({
13331365
required IMobileDevice iMobileDevice,
1366+
required Xcode? xcode,
13341367
bool useSyslog = true,
13351368
bool usingCISystem = false,
13361369
int? majorSdkVersion,
@@ -1339,6 +1372,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
13391372
}) {
13401373
final int sdkVersion = majorSdkVersion ?? (useSyslog ? 12 : 13);
13411374
return IOSDeviceLogReader._(
1375+
xcode,
13421376
iMobileDevice,
13431377
sdkVersion,
13441378
'1234',
@@ -1357,6 +1391,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
13571391
final bool _isWirelesslyConnected;
13581392
final bool _isCoreDevice;
13591393
final IMobileDevice _iMobileDevice;
1394+
final Xcode? _xcode;
13601395
final bool _usingCISystem;
13611396

13621397
// Matches a syslog line from the runner.
@@ -1448,6 +1483,15 @@ class IOSDeviceLogReader extends DeviceLogReader {
14481483
@override
14491484
Stream<String> get logLines => linesController.stream;
14501485

1486+
final _coreDeviceLoggingSource = CoreDeviceLoggingSource();
1487+
Future<void> listenToCoreDeviceLauncher(IOSCoreDeviceLauncher launcher) async {
1488+
if (!useCoreDeviceLogging) {
1489+
return;
1490+
}
1491+
_coreDeviceLoggingSource.coreDeviceLauncher = launcher;
1492+
await _coreDeviceLoggingSource.listenToLogs(addToLinesController, linesController);
1493+
}
1494+
14511495
FlutterVmService? _connectedVmService;
14521496

14531497
@override
@@ -1469,6 +1513,15 @@ class IOSDeviceLogReader extends DeviceLogReader {
14691513
// Also, `idevicesyslog` does not work with iOS 17 wireless devices, so use the
14701514
// Dart VM for wireless devices.
14711515
if (_isCoreDevice) {
1516+
// `idevicesyslog` stopped working with at least Xcode 26 (may have been before).
1517+
// Instead, use logging from `devicectl` and `lldb`.
1518+
final Version? xcodeVersion = _xcode?.currentVersion;
1519+
if (xcodeVersion != null && xcodeVersion.major >= 26) {
1520+
return _IOSDeviceLogSources(
1521+
primarySource: IOSDeviceLogSource.devicectlAndLldb,
1522+
fallbackSource: IOSDeviceLogSource.unifiedLogging,
1523+
);
1524+
}
14721525
if (_isWirelesslyConnected) {
14731526
return _IOSDeviceLogSources(primarySource: IOSDeviceLogSource.unifiedLogging);
14741527
}
@@ -1536,6 +1589,13 @@ class IOSDeviceLogReader extends DeviceLogReader {
15361589
logSources.fallbackSource == IOSDeviceLogSource.iosDeploy;
15371590
}
15381591

1592+
/// Whether `devicectl` and `lldb` are used as the primary or fallback source for device logs.
1593+
@visibleForTesting
1594+
bool get useCoreDeviceLogging {
1595+
return logSources.primarySource == IOSDeviceLogSource.devicectlAndLldb ||
1596+
logSources.fallbackSource == IOSDeviceLogSource.devicectlAndLldb;
1597+
}
1598+
15391599
/// Listen to Dart VM for logs on iOS 13 or greater.
15401600
Future<void> _listenToUnifiedLoggingEvents(FlutterVmService connectedVmService) async {
15411601
if (!useUnifiedLogging) {
@@ -1671,6 +1731,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
16711731
}
16721732
idevicesyslogProcess?.kill();
16731733
_iosDeployDebugger?.detach();
1734+
_coreDeviceLoggingSource.dispose();
16741735
}
16751736
}
16761737

@@ -1683,6 +1744,9 @@ enum IOSDeviceLogSource {
16831744

16841745
/// Gets logs from the Dart VM Service.
16851746
unifiedLogging,
1747+
1748+
/// Gets logs from `devicectl` and `lldb`
1749+
devicectlAndLldb,
16861750
}
16871751

16881752
class _IOSDeviceLogSources {
@@ -1692,6 +1756,62 @@ class _IOSDeviceLogSources {
16921756
final IOSDeviceLogSource? fallbackSource;
16931757
}
16941758

1759+
@visibleForTesting
1760+
class CoreDeviceLoggingSource {
1761+
IOSCoreDeviceLauncher? coreDeviceLauncher;
1762+
final _loggingSubscriptions = <StreamSubscription<void>>[];
1763+
1764+
Future<void> listenToLogs(
1765+
void Function(String, IOSDeviceLogSource) onLogMessage,
1766+
StreamController<String> linesController,
1767+
) async {
1768+
final IOSCoreDeviceLogForwarder? debugger = coreDeviceLauncher?.coreDeviceLogForwarder;
1769+
if (debugger != null) {
1770+
_loggingSubscriptions.add(
1771+
debugger.logLines.listen(
1772+
(String line) =>
1773+
onLogMessage(_debuggerLineHandler(line), IOSDeviceLogSource.devicectlAndLldb),
1774+
onError: linesController.addError,
1775+
onDone: linesController.close,
1776+
cancelOnError: true,
1777+
),
1778+
);
1779+
}
1780+
1781+
final LLDBLogForwarder? lldbLogForwarder = coreDeviceLauncher?.lldbLogForwarder;
1782+
if (lldbLogForwarder != null) {
1783+
_loggingSubscriptions.add(
1784+
lldbLogForwarder.logLines.listen(
1785+
(String line) =>
1786+
onLogMessage(_debuggerLineHandler(line), IOSDeviceLogSource.devicectlAndLldb),
1787+
onError: linesController.addError,
1788+
onDone: linesController.close,
1789+
cancelOnError: true,
1790+
),
1791+
);
1792+
}
1793+
}
1794+
1795+
// Logging from native code/Flutter engine is prefixed by timestamp and process metadata:
1796+
// 2020-09-15 19:15:10.931434-0700 Runner[541:226276] Did finish launching.
1797+
// 2020-09-15 19:15:10.931434-0700 Runner[541:226276] [Category] Did finish launching.
1798+
//
1799+
// Logging from the dart code has no prefixing metadata.
1800+
final _debuggerLoggingRegex = RegExp(r'^\S* \S* \S*\[[0-9:]*] (.*)');
1801+
1802+
// Strip off the logging metadata (leave the category), or just echo the line.
1803+
String _debuggerLineHandler(String line) =>
1804+
_debuggerLoggingRegex.firstMatch(line)?.group(1) ?? line;
1805+
1806+
void dispose() {
1807+
for (final StreamSubscription<void> loggingSubscription in _loggingSubscriptions) {
1808+
loggingSubscription.cancel();
1809+
}
1810+
coreDeviceLauncher?.lldbLogForwarder.exit();
1811+
coreDeviceLauncher?.coreDeviceLogForwarder.exit();
1812+
}
1813+
}
1814+
16951815
/// A [DevicePortForwarder] specialized for iOS usage with iproxy.
16961816
class IOSDevicePortForwarder extends DevicePortForwarder {
16971817
/// Create a new [IOSDevicePortForwarder].

0 commit comments

Comments
 (0)