diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index b9a02e8660..85d89aceeb 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -690,7 +690,7 @@ class IOSDevice extends Device { mDNSLookupTimer.cancel(); } } else { - if (isCoreDevice && vmServiceDiscovery != null) { + if ((isCoreDevice || forceXcodeDebugWorkflow) && vmServiceDiscovery != null) { // When searching for the Dart VM url, search for it via ProtocolDiscovery // (device logs) and mDNS simultaneously, since both can be flaky at times. final Future vmUrlFromMDns = MDnsVmServiceDiscovery.instance!.getVMServiceUriForLaunch( @@ -1071,7 +1071,8 @@ class IOSDeviceLogReader extends DeviceLogReader { // Sometimes (race condition?) we try to send a log after the controller has // been closed. See https://github.com/flutter/flutter/issues/99021 for more // context. - void _addToLinesController(String message, IOSDeviceLogSource source) { + @visibleForTesting + void addToLinesController(String message, IOSDeviceLogSource source) { if (!linesController.isClosed) { if (_excludeLog(message, source)) { return; @@ -1080,32 +1081,53 @@ class IOSDeviceLogReader extends DeviceLogReader { } } - /// Used to track messages prefixed with "flutter:" when [useBothLogDeviceReaders] - /// is true. - final List _streamFlutterMessages = []; + /// Used to track messages prefixed with "flutter:" from the fallback log source. + final List _fallbackStreamFlutterMessages = []; + + /// Used to track if a message prefixed with "flutter:" has been received from the primary log. + bool primarySourceFlutterLogReceived = false; /// There are three potential logging sources: `idevicesyslog`, `ios-deploy`, /// and Unified Logging (Dart VM). When using more than one of these logging - /// sources at a time, exclude logs with a `flutter:` prefix if they have - /// already been added to the stream. This is to prevent duplicates from - /// being printed. + /// sources at a time, prefer to use the primary source. However, if the + /// primary source is not working, use the fallback. bool _excludeLog(String message, IOSDeviceLogSource source) { - if (!usingMultipleLoggingSources) { + // If no fallback, don't exclude any logs. + if (logSources.fallbackSource == null) { return false; } - if (message.startsWith('flutter:')) { - if (_streamFlutterMessages.contains(message)) { + + // If log is from primary source, don't exclude it unless the fallback was + // quicker and added the message first. + if (source == logSources.primarySource) { + if (!primarySourceFlutterLogReceived && message.startsWith('flutter:')) { + primarySourceFlutterLogReceived = true; + } + + // If the message was already added by the fallback, exclude it to + // prevent duplicates. + final bool foundAndRemoved = _fallbackStreamFlutterMessages.remove(message); + if (foundAndRemoved) { return true; } - _streamFlutterMessages.add(message); - } else if (useIOSDeployLogging && source == IOSDeviceLogSource.idevicesyslog) { - // If using both `ios-deploy` and `idevicesyslog` simultaneously, exclude - // the message if its source is `idevicesyslog`. This is done because - //`ios-deploy` and `idevicesyslog` often have different prefixes, which - // makes duplicate matching difficult. Instead, exclude any non-flutter-prefixed - // `idevicesyslog` messages, which are not critical for CI tests. + return false; + } + + // If a flutter log was received from the primary source, that means it's + // working so don't use any messages from the fallback. + if (primarySourceFlutterLogReceived) { return true; } + + // When using logs from fallbacks, skip any logs not prefixed with "flutter:". + // This is done because different sources often have different prefixes for + // non-flutter messages, which makes duplicate matching difficult. Also, + // non-flutter messages are not critical for CI tests. + if (!message.startsWith('flutter:')) { + return true; + } + + _fallbackStreamFlutterMessages.add(message); return false; } @@ -1128,114 +1150,91 @@ class IOSDeviceLogReader extends DeviceLogReader { static const int minimumUniversalLoggingSdkVersion = 13; - /// Use `idevicesyslog` to stream logs from the device when one of the - /// following criteria is met: + /// Determine the primary and fallback source for device logs. /// - /// 1) The device is a physically attached CoreDevice. - /// 2) The device has iOS 16 or greater and it's being debugged from CI. - /// 3) The device has iOS 12 or lower. - /// - /// Syslog stopped working on iOS 13 (https://github.com/flutter/flutter/issues/41133). - /// However, from at least iOS 16, it has began working again. It's unclear - /// why it started working again. + /// There are three potential logging sources: `idevicesyslog`, `ios-deploy`, + /// and Unified Logging (Dart VM). @visibleForTesting - bool get useSyslogLogging { - // When forcing XcodeDebug workflow, use `idevicesyslog`. - if (_forceXcodeDebug) { - return true; - } - + _IOSDeviceLogSources get logSources { // `ios-deploy` stopped working with iOS 17 / Xcode 15, so use `idevicesyslog` instead. - // However, `idevicesyslog` does not work with iOS 17 wireless devices. - if (_isCoreDevice && !_isWirelesslyConnected) { - return true; + // However, `idevicesyslog` is sometimes unreliable so use Dart VM as a fallback. + // Also, `idevicesyslog` does not work with iOS 17 wireless devices, so use the + // Dart VM for wireless devices. + if (_isCoreDevice || _forceXcodeDebug) { + if (_isWirelesslyConnected) { + return _IOSDeviceLogSources( + primarySource: IOSDeviceLogSource.unifiedLogging, + ); + } + return _IOSDeviceLogSources( + primarySource: IOSDeviceLogSource.idevicesyslog, + fallbackSource: IOSDeviceLogSource.unifiedLogging, + ); } - // Use both logs from `idevicesyslog` and `ios-deploy` when debugging from CI system - // since sometimes `ios-deploy` does not return the device logs: + // Use `idevicesyslog` for iOS 12 or less. + // Syslog stopped working on iOS 13 (https://github.com/flutter/flutter/issues/41133). + // However, from at least iOS 16, it has began working again. It's unclear + // why it started working again. + if (_majorSdkVersion < minimumUniversalLoggingSdkVersion) { + return _IOSDeviceLogSources( + primarySource: IOSDeviceLogSource.idevicesyslog, + ); + } + + // Use `idevicesyslog` as a fallback to `ios-deploy` when debugging from + // CI system since sometimes `ios-deploy` does not return the device logs: // https://github.com/flutter/flutter/issues/121231 if (_usingCISystem && _majorSdkVersion >= 16) { - return true; + return _IOSDeviceLogSources( + primarySource: IOSDeviceLogSource.iosDeploy, + fallbackSource: IOSDeviceLogSource.idevicesyslog, + ); } - if (_majorSdkVersion < minimumUniversalLoggingSdkVersion) { - return true; + + // Use `ios-deploy` to stream logs from the device when the device is not a + // CoreDevice and has iOS 13 or greater. + // When using `ios-deploy` and the Dart VM, prefer the more complete logs + // from the attached debugger, if available. + if (connectedVMService != null && (_iosDeployDebugger == null || !_iosDeployDebugger!.debuggerAttached)) { + return _IOSDeviceLogSources( + primarySource: IOSDeviceLogSource.unifiedLogging, + fallbackSource: IOSDeviceLogSource.iosDeploy, + ); } - return false; + return _IOSDeviceLogSources( + primarySource: IOSDeviceLogSource.iosDeploy, + fallbackSource: IOSDeviceLogSource.unifiedLogging, + ); } - /// Use the Dart VM to stream logs from the device when one of the following - /// criteria is met: + /// Whether `idevicesyslog` is used as either the primary or fallback source for device logs. + @visibleForTesting + bool get useSyslogLogging { + return logSources.primarySource == IOSDeviceLogSource.idevicesyslog || + logSources.fallbackSource == IOSDeviceLogSource.idevicesyslog; + } + + /// Whether the Dart VM is used as either the primary or fallback source for device logs. /// - /// 1) The device is a CoreDevice and wirelessly connected. - /// 2) The device has iOS 13 or greater and [_iosDeployDebugger] is null or - /// the [_iosDeployDebugger] debugger is not attached. - /// - /// This value may change if [_iosDeployDebugger] changes. + /// Unified Logging only works after the Dart VM has been connected to. @visibleForTesting bool get useUnifiedLogging { - // Can't use Unified Logging if it's not going to listen to the Dart VM. - if (!_shouldListenForUnifiedLoggingEvents) { - return false; - } - - // `idevicesyslog` doesn't work on wireless devices, so use logs from Dart VM instead. - if (_isCoreDevice) { - return true; - } - - // Prefer the more complete logs from the attached debugger, if they are available. - if (_majorSdkVersion >= minimumUniversalLoggingSdkVersion && (_iosDeployDebugger == null || !_iosDeployDebugger!.debuggerAttached)) { - return true; - } - - return false; - } - - /// Determine whether to listen to the Dart VM for logging events. Returns - /// true when one of the following criteria is met: - /// - /// 1) The device is a CoreDevice and wirelessly connected. - /// 2) The device has iOS 13 or greater. - bool get _shouldListenForUnifiedLoggingEvents { - // `idevicesyslog` doesn't work on wireless devices, so use logs from Dart VM instead. - if (_isCoreDevice) { - return true; - } - - if (_majorSdkVersion >= minimumUniversalLoggingSdkVersion) { - return true; - } - - return false; + return logSources.primarySource == IOSDeviceLogSource.unifiedLogging || + logSources.fallbackSource == IOSDeviceLogSource.unifiedLogging; } - /// Use `ios-deploy` to stream logs from the device when the device is not a - /// CoreDevice and has iOS 13 or greater. + /// Whether `ios-deploy` is used as either the primary or fallback source for device logs. @visibleForTesting bool get useIOSDeployLogging { - if (_majorSdkVersion < minimumUniversalLoggingSdkVersion || _isCoreDevice) { - return false; - } - return true; - } - - @visibleForTesting - /// Returns true when using multiple sources for streaming the device logs. - bool get usingMultipleLoggingSources { - final int numberOfSources = (useIOSDeployLogging ? 1 : 0) + (useSyslogLogging ? 1 : 0) + (useUnifiedLogging ? 1 : 0); - if (numberOfSources > 1) { - return true; - } - return false; + return logSources.primarySource == IOSDeviceLogSource.iosDeploy || + logSources.fallbackSource == IOSDeviceLogSource.iosDeploy; } /// Listen to Dart VM for logs on iOS 13 or greater. - /// - /// Only send logs to stream if [_iosDeployDebugger] is null or - /// the [_iosDeployDebugger] debugger is not attached. Future _listenToUnifiedLoggingEvents(FlutterVmService connectedVmService) async { - if (!_shouldListenForUnifiedLoggingEvents) { + if (!useUnifiedLogging) { return; } try { @@ -1252,13 +1251,9 @@ class IOSDeviceLogReader extends DeviceLogReader { } void logMessage(vm_service.Event event) { - if (!useUnifiedLogging) { - // Prefer the more complete logs from the attached debugger. - return; - } final String message = processVmServiceMessage(event); if (message.isNotEmpty) { - _addToLinesController(message, IOSDeviceLogSource.unifiedLogging); + addToLinesController(message, IOSDeviceLogSource.unifiedLogging); } } @@ -1283,7 +1278,7 @@ class IOSDeviceLogReader extends DeviceLogReader { } // Add the debugger logs to the controller created on initialization. _loggingSubscriptions.add(debugger.logLines.listen( - (String line) => _addToLinesController( + (String line) => addToLinesController( _debuggerLineHandler(line), IOSDeviceLogSource.iosDeploy, ), @@ -1338,7 +1333,7 @@ class IOSDeviceLogReader extends DeviceLogReader { return (String line) { if (printing) { if (!_anyLineRegex.hasMatch(line)) { - _addToLinesController(decodeSyslog(line), IOSDeviceLogSource.idevicesyslog); + addToLinesController(decodeSyslog(line), IOSDeviceLogSource.idevicesyslog); return; } @@ -1350,7 +1345,7 @@ class IOSDeviceLogReader extends DeviceLogReader { if (match != null) { final String logLine = line.substring(match.end); // Only display the log line after the initial device and executable information. - _addToLinesController(decodeSyslog(logLine), IOSDeviceLogSource.idevicesyslog); + addToLinesController(decodeSyslog(logLine), IOSDeviceLogSource.idevicesyslog); printing = true; } }; @@ -1375,6 +1370,16 @@ enum IOSDeviceLogSource { unifiedLogging, } +class _IOSDeviceLogSources { + _IOSDeviceLogSources({ + required this.primarySource, + this.fallbackSource, + }); + + final IOSDeviceLogSource primarySource; + final IOSDeviceLogSource? fallbackSource; +} + /// A [DevicePortForwarder] specialized for iOS usage with iproxy. class IOSDevicePortForwarder extends DevicePortForwarder { diff --git a/packages/flutter_tools/lib/src/ios/xcode_debug.dart b/packages/flutter_tools/lib/src/ios/xcode_debug.dart index 2708add258..e1b5036435 100644 --- a/packages/flutter_tools/lib/src/ios/xcode_debug.dart +++ b/packages/flutter_tools/lib/src/ios/xcode_debug.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'package:meta/meta.dart'; import 'package:process/process.dart'; +import '../base/error_handling_io.dart'; import '../base/file_system.dart'; import '../base/io.dart'; import '../base/logger.dart'; @@ -186,7 +187,12 @@ class XcodeDebug { if (currentDebuggingProject != null) { final XcodeDebugProject project = currentDebuggingProject!; if (project.isTemporaryProject) { - project.xcodeProject.parent.deleteSync(recursive: true); + // Only delete if it exists. This is to prevent crashes when racing + // with shutdown hooks to delete temporary files. + ErrorHandlingFileSystem.deleteIfExists( + project.xcodeProject.parent, + recursive: true, + ); } currentDebuggingProject = null; } diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_logger_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_logger_test.dart index 8b25f528ab..01506cd99d 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_logger_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_logger_test.dart @@ -190,7 +190,7 @@ Runner(libsystem_asl.dylib)[297] : libMobileGestalt ])); }); - testWithoutContext('IOSDeviceLogReader ignores VM Service logs when attached to debugger', () async { + testWithoutContext('IOSDeviceLogReader ignores VM Service logs when attached to and received flutter logs from debugger', () async { final Event stdoutEvent = Event( kind: 'Stdout', timestamp: 0, @@ -229,14 +229,14 @@ Runner(libsystem_asl.dylib)[297] : libMobileGestalt iosDeployDebugger.debuggerAttached = true; final Stream debuggingLogs = Stream.fromIterable([ - 'Message from debugger', + 'flutter: Message from debugger', ]); iosDeployDebugger.logLines = debuggingLogs; logReader.debuggerStream = iosDeployDebugger; // Wait for stream listeners to fire. await expectLater(logReader.logLines, emitsInAnyOrder([ - equals('Message from debugger'), + equals('flutter: Message from debugger'), ])); }); }); @@ -365,7 +365,8 @@ Runner(libsystem_asl.dylib)[297] : libMobileGestalt expect(logReader.useSyslogLogging, isTrue); expect(logReader.useUnifiedLogging, isTrue); expect(logReader.useIOSDeployLogging, isFalse); - expect(logReader.usingMultipleLoggingSources, isTrue); + expect(logReader.logSources.primarySource, IOSDeviceLogSource.idevicesyslog); + expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.unifiedLogging); }); testWithoutContext('for wirelessly attached CoreDevice', () { @@ -384,7 +385,8 @@ Runner(libsystem_asl.dylib)[297] : libMobileGestalt expect(logReader.useSyslogLogging, isFalse); expect(logReader.useUnifiedLogging, isTrue); expect(logReader.useIOSDeployLogging, isFalse); - expect(logReader.usingMultipleLoggingSources, isFalse); + expect(logReader.logSources.primarySource, IOSDeviceLogSource.unifiedLogging); + expect(logReader.logSources.fallbackSource, isNull); }); testWithoutContext('for iOS 12 or less device', () { @@ -401,10 +403,11 @@ Runner(libsystem_asl.dylib)[297] : libMobileGestalt expect(logReader.useSyslogLogging, isTrue); expect(logReader.useUnifiedLogging, isFalse); expect(logReader.useIOSDeployLogging, isFalse); - expect(logReader.usingMultipleLoggingSources, isFalse); + expect(logReader.logSources.primarySource, IOSDeviceLogSource.idevicesyslog); + expect(logReader.logSources.fallbackSource, isNull); }); - testWithoutContext('for iOS 13 or greater non-CoreDevice', () { + testWithoutContext('for iOS 13 or greater non-CoreDevice and _iosDeployDebugger not attached', () { final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( iMobileDevice: IMobileDevice( artifacts: artifacts, @@ -418,7 +421,40 @@ Runner(libsystem_asl.dylib)[297] : libMobileGestalt expect(logReader.useSyslogLogging, isFalse); expect(logReader.useUnifiedLogging, isTrue); expect(logReader.useIOSDeployLogging, isTrue); - expect(logReader.usingMultipleLoggingSources, isTrue); + expect(logReader.logSources.primarySource, IOSDeviceLogSource.iosDeploy); + expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.unifiedLogging); + }); + + testWithoutContext('for iOS 13 or greater non-CoreDevice, _iosDeployDebugger not attached, and VM is connected', () { + final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( + iMobileDevice: IMobileDevice( + artifacts: artifacts, + processManager: processManager, + cache: fakeCache, + logger: logger, + ), + majorSdkVersion: 13, + ); + + final FlutterVmService vmService = FakeVmServiceHost(requests: [ + const FakeVmServiceRequest(method: 'streamListen', args: { + 'streamId': 'Debug', + }), + const FakeVmServiceRequest(method: 'streamListen', args: { + 'streamId': 'Stdout', + }), + const FakeVmServiceRequest(method: 'streamListen', args: { + 'streamId': 'Stderr', + }), + ]).vmService; + + logReader.connectedVMService = vmService; + + expect(logReader.useSyslogLogging, isFalse); + expect(logReader.useUnifiedLogging, isTrue); + expect(logReader.useIOSDeployLogging, isTrue); + expect(logReader.logSources.primarySource, IOSDeviceLogSource.unifiedLogging); + expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.iosDeploy); }); testWithoutContext('for iOS 13 or greater non-CoreDevice and _iosDeployDebugger is attached', () { @@ -436,10 +472,25 @@ Runner(libsystem_asl.dylib)[297] : libMobileGestalt iosDeployDebugger.debuggerAttached = true; logReader.debuggerStream = iosDeployDebugger; + final FlutterVmService vmService = FakeVmServiceHost(requests: [ + const FakeVmServiceRequest(method: 'streamListen', args: { + 'streamId': 'Debug', + }), + const FakeVmServiceRequest(method: 'streamListen', args: { + 'streamId': 'Stdout', + }), + const FakeVmServiceRequest(method: 'streamListen', args: { + 'streamId': 'Stderr', + }), + ]).vmService; + + logReader.connectedVMService = vmService; + expect(logReader.useSyslogLogging, isFalse); - expect(logReader.useUnifiedLogging, isFalse); + expect(logReader.useUnifiedLogging, isTrue); expect(logReader.useIOSDeployLogging, isTrue); - expect(logReader.usingMultipleLoggingSources, isFalse); + expect(logReader.logSources.primarySource, IOSDeviceLogSource.iosDeploy); + expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.unifiedLogging); }); testWithoutContext('for iOS 16 or greater non-CoreDevice', () { @@ -453,10 +504,15 @@ Runner(libsystem_asl.dylib)[297] : libMobileGestalt majorSdkVersion: 16, ); + final FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger(); + iosDeployDebugger.debuggerAttached = true; + logReader.debuggerStream = iosDeployDebugger; + expect(logReader.useSyslogLogging, isFalse); expect(logReader.useUnifiedLogging, isTrue); expect(logReader.useIOSDeployLogging, isTrue); - expect(logReader.usingMultipleLoggingSources, isTrue); + expect(logReader.logSources.primarySource, IOSDeviceLogSource.iosDeploy); + expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.unifiedLogging); }); testWithoutContext('for iOS 16 or greater non-CoreDevice in CI', () { @@ -472,126 +528,465 @@ Runner(libsystem_asl.dylib)[297] : libMobileGestalt ); expect(logReader.useSyslogLogging, isTrue); - expect(logReader.useUnifiedLogging, isTrue); - expect(logReader.useIOSDeployLogging, isTrue); - expect(logReader.usingMultipleLoggingSources, isTrue); - }); - - testWithoutContext('syslog sends flutter messages to stream when useSyslogLogging is true', () async { - processManager.addCommand( - FakeCommand( - command: [ - ideviceSyslogPath, '-u', '1234', - ], - stdout: ''' -Runner(Flutter)[297] : A is for ari -Runner(Flutter)[297] : I is for ichigo -May 30 13:56:28 Runner(Flutter)[2037] : flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/ -May 30 13:56:28 Runner(Flutter)[2037] : flutter: This is a test -May 30 13:56:28 Runner(Flutter)[2037] : [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend. -''' - ), - ); - final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( - iMobileDevice: IMobileDevice( - artifacts: artifacts, - processManager: processManager, - cache: fakeCache, - logger: logger, - ), - usingCISystem: true, - majorSdkVersion: 16, - ); - final List lines = await logReader.logLines.toList(); - - expect(logReader.useSyslogLogging, isTrue); - expect(processManager, hasNoRemainingExpectations); - expect(lines, [ - 'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/', - 'flutter: This is a test' - ]); - }); - - testWithoutContext('IOSDeviceLogReader only uses ios-deploy debugger when attached and not in CI', () async { - final Stream debuggingLogs = Stream.fromIterable([ - '(lldb) 2023-05-30 13:48:52.461894-0500 Runner[2019:1101495] [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend.', - '', - ]); - - final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( - iMobileDevice: IMobileDevice( - artifacts: artifacts, - processManager: processManager, - cache: fakeCache, - logger: logger, - ), - majorSdkVersion: 16, - ); - final FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger(); - iosDeployDebugger.debuggerAttached = true; - iosDeployDebugger.logLines = debuggingLogs; - logReader.debuggerStream = iosDeployDebugger; - final Future> logLines = logReader.logLines.toList(); - final List lines = await logLines; - - expect(logReader.useIOSDeployLogging, isTrue); - expect(logReader.useSyslogLogging, isFalse); expect(logReader.useUnifiedLogging, isFalse); - expect(logReader.usingMultipleLoggingSources, isFalse); - expect(processManager, hasNoRemainingExpectations); - expect( - lines.contains( - '(lldb) 2023-05-30 13:48:52.461894-0500 Runner[2019:1101495] [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend.', - ), - isTrue, - ); + expect(logReader.useIOSDeployLogging, isTrue); + expect(logReader.logSources.primarySource, IOSDeviceLogSource.iosDeploy); + expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.idevicesyslog); }); - testWithoutContext('IOSDeviceLogReader uses both syslog and ios-deploy debugger for CI and filters duplicate messages', () async { - processManager.addCommand( - FakeCommand( - command: [ - ideviceSyslogPath, '-u', '1234', - ], - stdout: ''' -May 30 13:56:28 Runner(Flutter)[2037] : flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/ -May 30 13:56:28 Runner(Flutter)[2037] : flutter: Check for duplicate -May 30 13:56:28 Runner(Flutter)[2037] : [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend. -''' - ), - ); + group('when useSyslogLogging', () { - final Stream debuggingLogs = Stream.fromIterable([ - '2023-06-01 12:49:01.445093-0500 Runner[2225:533240] flutter: Check for duplicate', - '(lldb) 2023-05-30 13:48:52.461894-0500 Runner[2019:1101495] [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend.', - ]); + testWithoutContext('is true syslog sends flutter messages to stream', () async { + processManager.addCommand( + FakeCommand( + command: [ + ideviceSyslogPath, '-u', '1234', + ], + stdout: ''' + Runner(Flutter)[297] : A is for ari + Runner(Flutter)[297] : I is for ichigo + May 30 13:56:28 Runner(Flutter)[2037] : flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/ + May 30 13:56:28 Runner(Flutter)[2037] : flutter: This is a test + May 30 13:56:28 Runner(Flutter)[2037] : [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend. + ''' + ), + ); + final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( + iMobileDevice: IMobileDevice( + artifacts: artifacts, + processManager: processManager, + cache: fakeCache, + logger: logger, + ), + usingCISystem: true, + majorSdkVersion: 16, + ); + final List lines = await logReader.logLines.toList(); - final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( - iMobileDevice: IMobileDevice( - artifacts: artifacts, - processManager: processManager, - cache: fakeCache, - logger: logger, - ), - usingCISystem: true, - majorSdkVersion: 16, - ); - final FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger(); - iosDeployDebugger.logLines = debuggingLogs; - logReader.debuggerStream = iosDeployDebugger; - final Future> logLines = logReader.logLines.toList(); - final List lines = await logLines; + expect(logReader.useSyslogLogging, isTrue); + expect(processManager, hasNoRemainingExpectations); + expect(lines, [ + 'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/', + 'flutter: This is a test' + ]); + }); - expect(logReader.useSyslogLogging, isTrue); - expect(logReader.useIOSDeployLogging, isTrue); - expect(logReader.usingMultipleLoggingSources, isTrue); - expect(processManager, hasNoRemainingExpectations); - expect(lines.length, 3); - expect(lines, containsAll([ - '(lldb) 2023-05-30 13:48:52.461894-0500 Runner[2019:1101495] [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend.', - 'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/', - 'flutter: Check for duplicate', - ])); + testWithoutContext('is false syslog does not send flutter messages to stream', () async { + final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( + iMobileDevice: IMobileDevice( + artifacts: artifacts, + processManager: processManager, + cache: fakeCache, + logger: logger, + ), + majorSdkVersion: 16, + ); + + final FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger(); + iosDeployDebugger.logLines = Stream.fromIterable([]); + logReader.debuggerStream = iosDeployDebugger; + + final List lines = await logReader.logLines.toList(); + + expect(logReader.useSyslogLogging, isFalse); + expect(processManager, hasNoRemainingExpectations); + expect(lines, isEmpty); + }); + }); + + group('when useIOSDeployLogging', () { + + testWithoutContext('is true ios-deploy sends flutter messages to stream', () async { + final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( + iMobileDevice: IMobileDevice( + artifacts: artifacts, + processManager: processManager, + cache: fakeCache, + logger: logger, + ), + majorSdkVersion: 16, + ); + + final FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger(); + final Stream debuggingLogs = Stream.fromIterable([ + 'flutter: Message from debugger', + ]); + iosDeployDebugger.logLines = debuggingLogs; + logReader.debuggerStream = iosDeployDebugger; + + final List lines = await logReader.logLines.toList(); + + expect(logReader.useIOSDeployLogging, isTrue); + expect(processManager, hasNoRemainingExpectations); + expect(lines, [ + 'flutter: Message from debugger', + ]); + }); + + testWithoutContext('is false ios-deploy does not send flutter messages to stream', () async { + final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( + iMobileDevice: IMobileDevice( + artifacts: artifacts, + processManager: FakeProcessManager.any(), + cache: fakeCache, + logger: logger, + ), + majorSdkVersion: 12, + ); + + final FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger(); + final Stream debuggingLogs = Stream.fromIterable([ + 'flutter: Message from debugger', + ]); + iosDeployDebugger.logLines = debuggingLogs; + logReader.debuggerStream = iosDeployDebugger; + + final List lines = await logReader.logLines.toList(); + + expect(logReader.useIOSDeployLogging, isFalse); + expect(processManager, hasNoRemainingExpectations); + expect(lines, isEmpty); + }); + }); + + group('when useUnifiedLogging', () { + + + testWithoutContext('is true Dart VM sends flutter messages to stream', () async { + final Event stdoutEvent = Event( + kind: 'Stdout', + timestamp: 0, + bytes: base64.encode(utf8.encode('flutter: A flutter message')), + ); + final Event stderrEvent = Event( + kind: 'Stderr', + timestamp: 0, + bytes: base64.encode(utf8.encode('flutter: A second flutter message')), + ); + final FlutterVmService vmService = FakeVmServiceHost(requests: [ + const FakeVmServiceRequest(method: 'streamListen', args: { + 'streamId': 'Debug', + }), + const FakeVmServiceRequest(method: 'streamListen', args: { + 'streamId': 'Stdout', + }), + const FakeVmServiceRequest(method: 'streamListen', args: { + 'streamId': 'Stderr', + }), + FakeVmServiceStreamResponse(event: stdoutEvent, streamId: 'Stdout'), + FakeVmServiceStreamResponse(event: stderrEvent, streamId: 'Stderr'), + ]).vmService; + final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( + useSyslog: false, + iMobileDevice: IMobileDevice( + artifacts: artifacts, + processManager: processManager, + cache: fakeCache, + logger: logger, + ), + ); + logReader.connectedVMService = vmService; + + // Wait for stream listeners to fire. + expect(logReader.useUnifiedLogging, isTrue); + expect(processManager, hasNoRemainingExpectations); + await expectLater(logReader.logLines, emitsInAnyOrder([ + equals('flutter: A flutter message'), + equals('flutter: A second flutter message'), + ])); + }); + + testWithoutContext('is false Dart VM does not send flutter messages to stream', () async { + final Event stdoutEvent = Event( + kind: 'Stdout', + timestamp: 0, + bytes: base64.encode(utf8.encode('flutter: A flutter message')), + ); + final Event stderrEvent = Event( + kind: 'Stderr', + timestamp: 0, + bytes: base64.encode(utf8.encode('flutter: A second flutter message')), + ); + final FlutterVmService vmService = FakeVmServiceHost(requests: [ + const FakeVmServiceRequest(method: 'streamListen', args: { + 'streamId': 'Debug', + }), + const FakeVmServiceRequest(method: 'streamListen', args: { + 'streamId': 'Stdout', + }), + const FakeVmServiceRequest(method: 'streamListen', args: { + 'streamId': 'Stderr', + }), + FakeVmServiceStreamResponse(event: stdoutEvent, streamId: 'Stdout'), + FakeVmServiceStreamResponse(event: stderrEvent, streamId: 'Stderr'), + ]).vmService; + final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( + iMobileDevice: IMobileDevice( + artifacts: artifacts, + processManager: FakeProcessManager.any(), + cache: fakeCache, + logger: logger, + ), + majorSdkVersion: 12, + ); + logReader.connectedVMService = vmService; + + final List lines = await logReader.logLines.toList(); + + // Wait for stream listeners to fire. + expect(logReader.useUnifiedLogging, isFalse); + expect(processManager, hasNoRemainingExpectations); + expect(lines, isEmpty); + }); + }); + + group('and when to exclude logs:', () { + + testWithoutContext('all primary messages are included except if fallback sent flutter message first', () async { + final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( + iMobileDevice: IMobileDevice( + artifacts: artifacts, + processManager: FakeProcessManager.any(), + cache: fakeCache, + logger: logger, + ), + usingCISystem: true, + majorSdkVersion: 16, + ); + + expect(logReader.useSyslogLogging, isTrue); + expect(logReader.useIOSDeployLogging, isTrue); + expect(logReader.logSources.primarySource, IOSDeviceLogSource.iosDeploy); + expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.idevicesyslog); + + final Future> logLines = logReader.logLines.toList(); + + logReader.addToLinesController( + 'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/', + IOSDeviceLogSource.idevicesyslog, + ); + // Will be excluded because was already added by fallback. + logReader.addToLinesController( + 'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/', + IOSDeviceLogSource.iosDeploy, + ); + logReader.addToLinesController( + 'A second non-flutter message', + IOSDeviceLogSource.iosDeploy, + ); + logReader.addToLinesController( + 'flutter: Another flutter message', + IOSDeviceLogSource.iosDeploy, + ); + final List lines = await logLines; + + expect(lines, containsAllInOrder([ + 'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/', // from idevicesyslog + 'A second non-flutter message', // from iosDeploy + 'flutter: Another flutter message', // from iosDeploy + ])); + }); + + testWithoutContext('all primary messages are included when there is no fallback', () async { + final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( + iMobileDevice: IMobileDevice( + artifacts: artifacts, + processManager: FakeProcessManager.any(), + cache: fakeCache, + logger: logger, + ), + majorSdkVersion: 12, + ); + + expect(logReader.useSyslogLogging, isTrue); + expect(logReader.logSources.primarySource, IOSDeviceLogSource.idevicesyslog); + expect(logReader.logSources.fallbackSource, isNull); + + final Future> logLines = logReader.logLines.toList(); + + logReader.addToLinesController( + 'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/', + IOSDeviceLogSource.idevicesyslog, + ); + logReader.addToLinesController( + 'A non-flutter message', + IOSDeviceLogSource.idevicesyslog, + ); + logReader.addToLinesController( + 'A non-flutter message', + IOSDeviceLogSource.idevicesyslog, + ); + logReader.addToLinesController( + 'flutter: A flutter message', + IOSDeviceLogSource.idevicesyslog, + ); + logReader.addToLinesController( + 'flutter: A flutter message', + IOSDeviceLogSource.idevicesyslog, + ); + final List lines = await logLines; + + expect(lines, containsAllInOrder([ + 'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/', + 'A non-flutter message', + 'A non-flutter message', + 'flutter: A flutter message', + 'flutter: A flutter message', + ])); + }); + + testWithoutContext('primary messages are not added if fallback already added them, otherwise duplicates are allowed', () async { + final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( + iMobileDevice: IMobileDevice( + artifacts: artifacts, + processManager: FakeProcessManager.any(), + cache: fakeCache, + logger: logger, + ), + usingCISystem: true, + majorSdkVersion: 16, + ); + + expect(logReader.useSyslogLogging, isTrue); + expect(logReader.useIOSDeployLogging, isTrue); + expect(logReader.logSources.primarySource, IOSDeviceLogSource.iosDeploy); + expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.idevicesyslog); + + final Future> logLines = logReader.logLines.toList(); + + logReader.addToLinesController( + 'flutter: A flutter message', + IOSDeviceLogSource.idevicesyslog, + ); + logReader.addToLinesController( + 'flutter: A flutter message', + IOSDeviceLogSource.idevicesyslog, + ); + logReader.addToLinesController( + 'A non-flutter message', + IOSDeviceLogSource.iosDeploy, + ); + logReader.addToLinesController( + 'A non-flutter message', + IOSDeviceLogSource.iosDeploy, + ); + // Will be excluded because was already added by fallback. + logReader.addToLinesController( + 'flutter: A flutter message', + IOSDeviceLogSource.iosDeploy, + ); + // Will be excluded because was already added by fallback. + logReader.addToLinesController( + 'flutter: A flutter message', + IOSDeviceLogSource.iosDeploy, + ); + // Will be included because, although the message is the same, the + // fallback only added it twice so this third one is considered new. + logReader.addToLinesController( + 'flutter: A flutter message', + IOSDeviceLogSource.iosDeploy, + ); + + final List lines = await logLines; + + expect(lines, containsAllInOrder([ + 'flutter: A flutter message', // from idevicesyslog + 'flutter: A flutter message', // from idevicesyslog + 'A non-flutter message', // from iosDeploy + 'A non-flutter message', // from iosDeploy + 'flutter: A flutter message', // from iosDeploy + ])); + }); + + testWithoutContext('flutter fallback messages are included until a primary flutter message is received', () async { + final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( + iMobileDevice: IMobileDevice( + artifacts: artifacts, + processManager: FakeProcessManager.any(), + cache: fakeCache, + logger: logger, + ), + usingCISystem: true, + majorSdkVersion: 16, + ); + + expect(logReader.useSyslogLogging, isTrue); + expect(logReader.useIOSDeployLogging, isTrue); + expect(logReader.logSources.primarySource, IOSDeviceLogSource.iosDeploy); + expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.idevicesyslog); + + final Future> logLines = logReader.logLines.toList(); + + logReader.addToLinesController( + 'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/', + IOSDeviceLogSource.idevicesyslog, + ); + logReader.addToLinesController( + 'A second non-flutter message', + IOSDeviceLogSource.iosDeploy, + ); + // Will be included because the first log from primary source wasn't a + // flutter log. + logReader.addToLinesController( + 'flutter: A flutter message', + IOSDeviceLogSource.idevicesyslog, + ); + // Will be excluded because was already added by fallback, however, it + // will be used to determine a flutter log was received by the primary source. + logReader.addToLinesController( + 'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/', + IOSDeviceLogSource.iosDeploy, + ); + // Will be excluded because flutter log from primary was received. + logReader.addToLinesController( + 'flutter: A third flutter message', + IOSDeviceLogSource.idevicesyslog, + ); + + final List lines = await logLines; + + expect(lines, containsAllInOrder([ + 'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/', // from idevicesyslog + 'A second non-flutter message', // from iosDeploy + 'flutter: A flutter message', // from idevicesyslog + ])); + }); + + testWithoutContext('non-flutter fallback messages are not included', () async { + final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( + iMobileDevice: IMobileDevice( + artifacts: artifacts, + processManager: FakeProcessManager.any(), + cache: fakeCache, + logger: logger, + ), + usingCISystem: true, + majorSdkVersion: 16, + ); + + expect(logReader.useSyslogLogging, isTrue); + expect(logReader.useIOSDeployLogging, isTrue); + expect(logReader.logSources.primarySource, IOSDeviceLogSource.iosDeploy); + expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.idevicesyslog); + + final Future> logLines = logReader.logLines.toList(); + + logReader.addToLinesController( + 'flutter: A flutter message', + IOSDeviceLogSource.idevicesyslog, + ); + // Will be excluded because it's from fallback and not a flutter message. + logReader.addToLinesController( + 'A non-flutter message', + IOSDeviceLogSource.idevicesyslog, + ); + + final List lines = await logLines; + + expect(lines, containsAllInOrder([ + 'flutter: A flutter message', + ])); + }); }); }); } diff --git a/packages/flutter_tools/test/general.shard/ios/xcode_debug_test.dart b/packages/flutter_tools/test/general.shard/ios/xcode_debug_test.dart index 67aa5838af..cbd2416c2d 100644 --- a/packages/flutter_tools/test/general.shard/ios/xcode_debug_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/xcode_debug_test.dart @@ -784,6 +784,69 @@ void main() { expect(status, isTrue); }); + testWithoutContext('prints error message when deleting temporary directory that is nonexistant', () async { + final Xcode xcode = setupXcode( + fakeProcessManager: fakeProcessManager, + fileSystem: fileSystem, + flutterRoot: flutterRoot, + ); + final XcodeDebugProject project = XcodeDebugProject( + scheme: 'Runner', + xcodeProject: xcodeproj, + xcodeWorkspace: xcworkspace, + isTemporaryProject: true, + ); + final XcodeDebug xcodeDebug = XcodeDebug( + logger: logger, + processManager: fakeProcessManager, + xcode: xcode, + fileSystem: fileSystem, + ); + + fakeProcessManager.addCommands([ + FakeCommand( + command: [ + 'xcrun', + 'osascript', + '-l', + 'JavaScript', + pathToXcodeAutomationScript, + 'stop', + '--xcode-path', + pathToXcodeApp, + '--project-path', + project.xcodeProject.path, + '--workspace-path', + project.xcodeWorkspace.path, + '--close-window' + ], + stdout: ''' + {"status":true,"errorMessage":null,"debugResult":null} + ''', + ), + ]); + + xcodeDebug.startDebugActionProcess = FakeProcess(); + xcodeDebug.currentDebuggingProject = project; + + expect(xcodeDebug.startDebugActionProcess, isNotNull); + expect(xcodeDebug.currentDebuggingProject, isNotNull); + expect(projectDirectory.existsSync(), isFalse); + expect(xcodeproj.existsSync(), isFalse); + expect(xcworkspace.existsSync(), isFalse); + + final bool status = await xcodeDebug.exit(skipDelay: true); + + expect((xcodeDebug.startDebugActionProcess! as FakeProcess).killed, isTrue); + expect(xcodeDebug.currentDebuggingProject, isNull); + expect(projectDirectory.existsSync(), isFalse); + expect(xcodeproj.existsSync(), isFalse); + expect(xcworkspace.existsSync(), isFalse); + expect(logger.errorText, contains('Failed to delete temporary Xcode project')); + expect(fakeProcessManager, hasNoRemainingExpectations); + expect(status, isTrue); + }); + testWithoutContext('kill Xcode when force exit', () async { final Xcode xcode = setupXcode( fakeProcessManager: FakeProcessManager.any(), @@ -825,6 +888,46 @@ void main() { expect(fakeProcessManager, hasNoRemainingExpectations); expect(exitStatus, isTrue); }); + + testWithoutContext('does not crash when deleting temporary directory that is nonexistant when force exiting', () async { + final Xcode xcode = setupXcode( + fakeProcessManager: FakeProcessManager.any(), + fileSystem: fileSystem, + flutterRoot: flutterRoot, + ); + final XcodeDebugProject project = XcodeDebugProject( + scheme: 'Runner', + xcodeProject: xcodeproj, + xcodeWorkspace: xcworkspace, + isTemporaryProject: true, + ); + final XcodeDebug xcodeDebug = XcodeDebug( + logger: logger, + processManager:FakeProcessManager.any(), + xcode: xcode, + fileSystem: fileSystem, + ); + + xcodeDebug.startDebugActionProcess = FakeProcess(); + xcodeDebug.currentDebuggingProject = project; + + expect(xcodeDebug.startDebugActionProcess, isNotNull); + expect(xcodeDebug.currentDebuggingProject, isNotNull); + expect(projectDirectory.existsSync(), isFalse); + expect(xcodeproj.existsSync(), isFalse); + expect(xcworkspace.existsSync(), isFalse); + + final bool status = await xcodeDebug.exit(force: true); + + expect((xcodeDebug.startDebugActionProcess! as FakeProcess).killed, isTrue); + expect(xcodeDebug.currentDebuggingProject, isNull); + expect(projectDirectory.existsSync(), isFalse); + expect(xcodeproj.existsSync(), isFalse); + expect(xcworkspace.existsSync(), isFalse); + expect(logger.errorText, isEmpty); + expect(fakeProcessManager, hasNoRemainingExpectations); + expect(status, isTrue); + }); }); group('stop app', () {