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+
58import 'dart:async' ;
69
710import 'package:meta/meta.dart' ;
@@ -28,6 +31,7 @@ import '../device_vm_service_discovery_for_attach.dart';
2831import '../features.dart' ;
2932import '../globals.dart' as globals;
3033import '../macos/xcdevice.dart' ;
34+ import '../macos/xcode.dart' ;
3135import '../mdns_discovery.dart' ;
3236import '../project.dart' ;
3337import '../protocol_discovery.dart' ;
@@ -37,6 +41,7 @@ import 'core_devices.dart';
3741import 'ios_deploy.dart' ;
3842import 'ios_workflow.dart' ;
3943import 'iproxy.dart' ;
44+ import 'lldb.dart' ;
4045import 'mac.dart' ;
4146import 'xcode_build_settings.dart' ;
4247import '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] .
12941323class 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
16881752class _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.
16961816class IOSDevicePortForwarder extends DevicePortForwarder {
16971817 /// Create a new [IOSDevicePortForwarder] .
0 commit comments