diff --git a/dev/devicelab/lib/framework/utils.dart b/dev/devicelab/lib/framework/utils.dart index 159cad2363..9d19e6863c 100644 --- a/dev/devicelab/lib/framework/utils.dart +++ b/dev/devicelab/lib/framework/utils.dart @@ -469,6 +469,7 @@ List _flutterCommandArgs(String command, List options) { final String? localEngineHost = localEngineHostFromEnv; final String? localEngineSrcPath = localEngineSrcPathFromEnv; final String? localWebSdk = localWebSdkFromEnv; + final bool pubOrPackagesCommand = command.startsWith('packages') || command.startsWith('pub'); return [ command, if (deviceOperatingSystem == DeviceOperatingSystem.ios && supportedDeviceTimeoutCommands.contains(command)) @@ -489,7 +490,9 @@ List _flutterCommandArgs(String command, List options) { // Use CI flag when running devicelab tests, except for `packages`/`pub` commands. // `packages`/`pub` commands effectively runs the `pub` tool, which does not have // the same allowed args. - if (!command.startsWith('packages') && !command.startsWith('pub')) '--ci', + if (!pubOrPackagesCommand) '--ci', + if (!pubOrPackagesCommand && hostAgent.dumpDirectory != null) + '--debug-logs-dir=${hostAgent.dumpDirectory!.path}' ]; } diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart index 04a0d91e46..c5a875db79 100644 --- a/packages/flutter_tools/lib/src/commands/attach.dart +++ b/packages/flutter_tools/lib/src/commands/attach.dart @@ -525,6 +525,7 @@ known, it can be explicitly provided to attach via the command-line, e.g. devToolsServerAddress: devToolsServerAddress, serveObservatory: serveObservatory, usingCISystem: usingCISystem, + debugLogsDirectoryPath: debugLogsDirectoryPath, ); return buildInfo.isDebug diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index 9759d4ef1f..03284538d3 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -264,6 +264,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment enableDartProfiling: enableDartProfiling, enableEmbedderApi: enableEmbedderApi, usingCISystem: usingCISystem, + debugLogsDirectoryPath: debugLogsDirectoryPath, ); } else { return DebuggingOptions.enabled( @@ -319,6 +320,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment enableDartProfiling: enableDartProfiling, enableEmbedderApi: enableEmbedderApi, usingCISystem: usingCISystem, + debugLogsDirectoryPath: debugLogsDirectoryPath, ); } } diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart index 25d7e05019..2bed07aa13 100644 --- a/packages/flutter_tools/lib/src/commands/test.dart +++ b/packages/flutter_tools/lib/src/commands/test.dart @@ -364,6 +364,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { nullAssertions: boolArg(FlutterOptions.kNullAssertions), usingCISystem: usingCISystem, enableImpeller: ImpellerStatus.fromBool(argResults!['enable-impeller'] as bool?), + debugLogsDirectoryPath: debugLogsDirectoryPath, ); String? testAssetDirectory; diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 2639448d0d..69938a9b78 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -963,6 +963,7 @@ class DebuggingOptions { this.enableDartProfiling = true, this.enableEmbedderApi = false, this.usingCISystem = false, + this.debugLogsDirectoryPath, }) : debuggingEnabled = true; DebuggingOptions.disabled(this.buildInfo, { @@ -988,6 +989,7 @@ class DebuggingOptions { this.enableDartProfiling = true, this.enableEmbedderApi = false, this.usingCISystem = false, + this.debugLogsDirectoryPath, }) : debuggingEnabled = false, useTestFonts = false, startPaused = false, @@ -1069,6 +1071,7 @@ class DebuggingOptions { required this.enableDartProfiling, required this.enableEmbedderApi, required this.usingCISystem, + required this.debugLogsDirectoryPath, }); final bool debuggingEnabled; @@ -1112,6 +1115,7 @@ class DebuggingOptions { final bool enableDartProfiling; final bool enableEmbedderApi; final bool usingCISystem; + final String? debugLogsDirectoryPath; /// Whether the tool should try to uninstall a previously installed version of the app. /// @@ -1258,6 +1262,7 @@ class DebuggingOptions { 'enableDartProfiling': enableDartProfiling, 'enableEmbedderApi': enableEmbedderApi, 'usingCISystem': usingCISystem, + 'debugLogsDirectoryPath': debugLogsDirectoryPath, }; static DebuggingOptions fromJson(Map json, BuildInfo buildInfo) => @@ -1313,6 +1318,7 @@ class DebuggingOptions { enableDartProfiling: (json['enableDartProfiling'] as bool?) ?? true, enableEmbedderApi: (json['enableEmbedderApi'] as bool?) ?? false, usingCISystem: (json['usingCISystem'] as bool?) ?? false, + debugLogsDirectoryPath: json['debugLogsDirectoryPath'] as String?, ); } diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index 9f6eee7ffc..6682872855 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -620,105 +620,75 @@ class IOSDevice extends Device { }); Uri? localUri; - if (isWirelesslyConnected) { - // When using a CoreDevice, device logs are unavailable and therefore - // cannot be used to get the Dart VM url. Instead, get the Dart VM - // Service by finding services matching the app bundle id and the - // device name. - // - // If not using a CoreDevice, wait for the Dart VM url to be discovered - // via logs and then get the Dart VM Service by finding services matching - // the app bundle id and the Dart VM port. - // - // Then in both cases, get the device IP from the Dart VM Service to - // construct the Dart VM url using the device IP as the host. - if (isCoreDevice) { - localUri = await MDnsVmServiceDiscovery.instance!.getVMServiceUriForLaunch( - packageId, - this, - usesIpv6: ipv6, - useDeviceIPAsHost: true, + if (isCoreDevice || forceXcodeDebugWorkflow) { + localUri = await _discoverDartVMForCoreDevice( + debuggingOptions: debuggingOptions, + packageId: packageId, + ipv6: ipv6, + vmServiceDiscovery: vmServiceDiscovery, + ); + } else if (isWirelesslyConnected) { + // Wait for the Dart VM url to be discovered via logs (from `ios-deploy`) + // in ProtocolDiscovery. Then via mDNS, construct the Dart VM url using + // the device IP as the host by finding Dart VM services matching the + // app bundle id and Dart VM port. + + // Wait for Dart VM Service to start up. + final Uri? serviceURL = await vmServiceDiscovery?.uri; + if (serviceURL == null) { + await iosDeployDebugger?.stopAndDumpBacktrace(); + await dispose(); + return LaunchResult.failed(); + } + + // If Dart VM Service URL with the device IP is not found within 5 seconds, + // change the status message to prompt users to click Allow. Wait 5 seconds because it + // should only show this message if they have not already approved the permissions. + // MDnsVmServiceDiscovery usually takes less than 5 seconds to find it. + final Timer mDNSLookupTimer = Timer(const Duration(seconds: 5), () { + startAppStatus.stop(); + startAppStatus = _logger.startProgress( + 'Waiting for approval of local network permissions...', ); - } else { - // Wait for Dart VM Service to start up. - final Uri? serviceURL = await vmServiceDiscovery?.uri; - if (serviceURL == null) { - await iosDeployDebugger?.stopAndDumpBacktrace(); + }); + + // Get Dart VM Service URL with the device IP as the host. + localUri = await MDnsVmServiceDiscovery.instance!.getVMServiceUriForLaunch( + packageId, + this, + usesIpv6: ipv6, + deviceVmservicePort: serviceURL.port, + useDeviceIPAsHost: true, + ); + + mDNSLookupTimer.cancel(); + } else { + localUri = await vmServiceDiscovery?.uri; + // If the `ios-deploy` debugger loses connection before it finds the + // Dart Service VM url, try starting the debugger and launching the + // app again. + if (localUri == null && + debuggingOptions.usingCISystem && + iosDeployDebugger != null && + iosDeployDebugger!.lostConnection) { + _logger.printStatus('Lost connection to device. Trying to connect again...'); + await dispose(); + vmServiceDiscovery = _setupDebuggerAndVmServiceDiscovery( + package: package, + bundle: bundle, + debuggingOptions: debuggingOptions, + launchArguments: launchArguments, + ipv6: ipv6, + uninstallFirst: false, + skipInstall: true, + ); + installationResult = await iosDeployDebugger!.launchAndAttach() ? 0 : 1; + if (installationResult != 0) { + _printInstallError(bundle); await dispose(); return LaunchResult.failed(); } - - // If Dart VM Service URL with the device IP is not found within 5 seconds, - // change the status message to prompt users to click Allow. Wait 5 seconds because it - // should only show this message if they have not already approved the permissions. - // MDnsVmServiceDiscovery usually takes less than 5 seconds to find it. - final Timer mDNSLookupTimer = Timer(const Duration(seconds: 5), () { - startAppStatus.stop(); - startAppStatus = _logger.startProgress( - 'Waiting for approval of local network permissions...', - ); - }); - - // Get Dart VM Service URL with the device IP as the host. - localUri = await MDnsVmServiceDiscovery.instance!.getVMServiceUriForLaunch( - packageId, - this, - usesIpv6: ipv6, - deviceVmservicePort: serviceURL.port, - useDeviceIPAsHost: true, - ); - - mDNSLookupTimer.cancel(); - } - } else { - 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( - packageId, - this, - usesIpv6: ipv6, - ); - final Future vmUrlFromLogs = vmServiceDiscovery.uri; - localUri = await Future.any( - >[vmUrlFromMDns, vmUrlFromLogs] - ); - - // If the first future to return is null, wait for the other to complete. - if (localUri == null) { - final List vmUrls = await Future.wait( - >[vmUrlFromMDns, vmUrlFromLogs] - ); - localUri = vmUrls.where((Uri? vmUrl) => vmUrl != null).firstOrNull; - } - } else { - localUri = await vmServiceDiscovery?.uri; - // If the `ios-deploy` debugger loses connection before it finds the - // Dart Service VM url, try starting the debugger and launching the - // app again. - if (localUri == null && - debuggingOptions.usingCISystem && - iosDeployDebugger != null && - iosDeployDebugger!.lostConnection) { - _logger.printStatus('Lost connection to device. Trying to connect again...'); - await dispose(); - vmServiceDiscovery = _setupDebuggerAndVmServiceDiscovery( - package: package, - bundle: bundle, - debuggingOptions: debuggingOptions, - launchArguments: launchArguments, - ipv6: ipv6, - uninstallFirst: false, - skipInstall: true, - ); - installationResult = await iosDeployDebugger!.launchAndAttach() ? 0 : 1; - if (installationResult != 0) { - _printInstallError(bundle); - await dispose(); - return LaunchResult.failed(); - } - localUri = await vmServiceDiscovery.uri; - } + localUri = await vmServiceDiscovery.uri; } } timer.cancel(); @@ -757,6 +727,96 @@ class IOSDevice extends Device { _logger.printError(''); } + /// Find the Dart VM url using ProtocolDiscovery (logs from `idevicesyslog`) + /// and mDNS simultaneously, using whichever is found first. `idevicesyslog` + /// does not work on wireless devices, so only use mDNS for wireless devices. + /// Wireless devices require using the device IP as the host. + Future _discoverDartVMForCoreDevice({ + required String packageId, + required bool ipv6, + required DebuggingOptions debuggingOptions, + ProtocolDiscovery? vmServiceDiscovery, + }) async { + Timer? maxWaitForCI; + final Completer cancelCompleter = Completer(); + + // When testing in CI, wait a max of 10 minutes for the Dart VM to be found. + // Afterwards, stop the app from running and upload DerivedData Logs to debug + // logs directory. CoreDevices are run through Xcode and launch logs are + // therefore found in DerivedData. + if (debuggingOptions.usingCISystem && debuggingOptions.debugLogsDirectoryPath != null) { + maxWaitForCI = Timer(const Duration(minutes: 10), () async { + _logger.printError('Failed to find Dart VM after 10 minutes.'); + await _xcodeDebug.exit(); + final String? homePath = _platform.environment['HOME']; + Directory? derivedData; + if (homePath != null) { + derivedData = _fileSystem.directory( + _fileSystem.path.join(homePath, 'Library', 'Developer', 'Xcode', 'DerivedData'), + ); + } + if (derivedData != null && derivedData.existsSync()) { + final Directory debugLogsDirectory = _fileSystem.directory( + debuggingOptions.debugLogsDirectoryPath, + ); + debugLogsDirectory.createSync(recursive: true); + for (final FileSystemEntity entity in derivedData.listSync()) { + if (entity is! Directory || !entity.childDirectory('Logs').existsSync()) { + continue; + } + final Directory logsToCopy = entity.childDirectory('Logs'); + final Directory copyDestination = debugLogsDirectory + .childDirectory('DerivedDataLogs') + .childDirectory(entity.basename) + .childDirectory('Logs'); + _logger.printTrace('Copying logs ${logsToCopy.path} to ${copyDestination.path}...'); + copyDirectory(logsToCopy, copyDestination); + } + } + cancelCompleter.complete(); + }); + } + + final Future vmUrlFromMDns = MDnsVmServiceDiscovery.instance!.getVMServiceUriForLaunch( + packageId, + this, + usesIpv6: ipv6, + useDeviceIPAsHost: isWirelesslyConnected, + ); + + final List> discoveryOptions = >[ + vmUrlFromMDns, + ]; + + // vmServiceDiscovery uses device logs (`idevicesyslog`), which doesn't work + // on wireless devices. + if (vmServiceDiscovery != null && !isWirelesslyConnected) { + final Future vmUrlFromLogs = vmServiceDiscovery.uri; + discoveryOptions.add(vmUrlFromLogs); + } + + Uri? localUri = await Future.any( + >[...discoveryOptions, cancelCompleter.future], + ); + + // If the first future to return is null, wait for the other to complete + // unless canceled. + if (localUri == null && !cancelCompleter.isCompleted) { + final Future> allDiscoveryOptionsComplete = Future.wait(discoveryOptions); + await Future.any(>[ + allDiscoveryOptionsComplete, + cancelCompleter.future, + ]); + if (!cancelCompleter.isCompleted) { + // If it wasn't cancelled, that means one of the discovery options completed. + final List vmUrls = await allDiscoveryOptionsComplete; + localUri = vmUrls.where((Uri? vmUrl) => vmUrl != null).firstOrNull; + } + } + maxWaitForCI?.cancel(); + return localUri; + } + ProtocolDiscovery _setupDebuggerAndVmServiceDiscovery({ required IOSApp package, required Directory bundle, diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart index ee8a2104d1..905b0fa254 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart @@ -377,6 +377,8 @@ abstract class FlutterCommand extends Command { /// Whether flutter is being run from our CI. bool get usingCISystem => boolArg(FlutterGlobalOptions.kContinuousIntegrationFlag, global: true); + String? get debugLogsDirectoryPath => stringArg(FlutterGlobalOptions.kDebugLogsDirectoryFlag, global: true); + /// The value of the `--filesystem-scheme` argument. /// /// This can be overridden by some of its subclasses. diff --git a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart index 3526937ac0..2970950a60 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart @@ -44,6 +44,7 @@ abstract final class FlutterGlobalOptions { static const String kVersionFlag = 'version'; static const String kWrapColumnOption = 'wrap-column'; static const String kWrapFlag = 'wrap'; + static const String kDebugLogsDirectoryFlag = 'debug-logs-dir'; } class FlutterCommandRunner extends CommandRunner { @@ -164,6 +165,11 @@ class FlutterCommandRunner extends CommandRunner { help: 'Enable a set of CI-specific test debug settings.', hide: !verboseHelp, ); + argParser.addOption( + FlutterGlobalOptions.kDebugLogsDirectoryFlag, + help: 'Path to a directory where logs for debugging may be added.', + hide: !verboseHelp, + ); } @override diff --git a/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart index a8d11116ed..50ffec8db5 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart @@ -427,6 +427,7 @@ void main() { '--skia-deterministic-rendering', '--enable-embedder-api', '--ci', + '--debug-logs-dir=path/to/logs' ]), throwsToolExit()); final DebuggingOptions options = await command.createDebuggingOptions(false); @@ -444,6 +445,7 @@ void main() { expect(options.enableSoftwareRendering, true); expect(options.skiaDeterministicRendering, true); expect(options.usingCISystem, true); + expect(options.debugLogsDirectoryPath, 'path/to/logs'); }, overrides: { Cache: () => Cache.test(processManager: FakeProcessManager.any()), FileSystem: () => MemoryFileSystem.test(), diff --git a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart index 510adf1c3f..8c097e6c51 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart @@ -1262,6 +1262,7 @@ void main() { '--skia-deterministic-rendering', '--enable-embedder-api', '--ci', + '--debug-logs-dir=path/to/logs' ]), throwsToolExit()); final DebuggingOptions options = await command.createDebuggingOptions(false); @@ -1281,6 +1282,7 @@ void main() { expect(options.enableSoftwareRendering, true); expect(options.skiaDeterministicRendering, true); expect(options.usingCISystem, true); + expect(options.debugLogsDirectoryPath, 'path/to/logs'); }, overrides: { Cache: () => Cache.test(processManager: FakeProcessManager.any()), FileSystem: () => MemoryFileSystem.test(), diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart index 0e03fb9329..0325f55172 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart @@ -966,6 +966,78 @@ void main() { MDnsVmServiceDiscovery: () => mdnsDiscovery, }); }); + + testUsingContext('IOSDevice.startApp fails to find Dart VM in CI', () async { + final FileSystem fileSystem = MemoryFileSystem.test(); + final FakeProcessManager processManager = FakeProcessManager.empty(); + + const String pathToFlutterLogs = '/path/to/flutter/logs'; + const String pathToHome = '/path/to/home'; + + final Directory temporaryXcodeProjectDirectory = fileSystem.systemTempDirectory.childDirectory('flutter_empty_xcode.rand0'); + final Directory bundleLocation = fileSystem.currentDirectory; + final IOSDevice device = setUpIOSDevice( + processManager: processManager, + fileSystem: fileSystem, + isCoreDevice: true, + coreDeviceControl: FakeIOSCoreDeviceControl(), + xcodeDebug: FakeXcodeDebug( + expectedProject: XcodeDebugProject( + scheme: 'Runner', + xcodeWorkspace: temporaryXcodeProjectDirectory.childDirectory('Runner.xcworkspace'), + xcodeProject: temporaryXcodeProjectDirectory.childDirectory('Runner.xcodeproj'), + hostAppProjectName: 'Runner', + ), + expectedDeviceId: '123', + expectedLaunchArguments: ['--enable-dart-profiling'], + expectedBundlePath: bundleLocation.path, + ), + platform: FakePlatform( + operatingSystem: 'macos', + environment: { + 'HOME': pathToHome, + }, + ), + ); + + final IOSApp iosApp = PrebuiltIOSApp( + projectBundleId: 'app', + bundleName: 'Runner', + uncompressedBundle: bundleLocation, + applicationPackage: bundleLocation, + ); + final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader(); + + device.portForwarder = const NoOpDevicePortForwarder(); + device.setLogReader(iosApp, deviceLogReader); + + const String projectLogsPath = 'Runner-project1/Logs/Launch/Runner.xcresults'; + fileSystem.directory('$pathToHome/Library/Developer/Xcode/DerivedData/$projectLogsPath').createSync(recursive: true); + + final Completer completer = Completer(); + await FakeAsync().run((FakeAsync time) { + final Future futureLaunchResult = device.startApp(iosApp, + prebuiltApplication: true, + debuggingOptions: DebuggingOptions.enabled( + BuildInfo.debug, + usingCISystem: true, + debugLogsDirectoryPath: pathToFlutterLogs, + ), + platformArgs: {}, + ); + futureLaunchResult.then((LaunchResult launchResult) { + expect(launchResult.started, false); + expect(launchResult.hasVmService, false); + expect(fileSystem.directory('$pathToFlutterLogs/DerivedDataLogs/$projectLogsPath').existsSync(), true); + completer.complete(); + }); + time.elapse(const Duration(minutes: 15)); + time.flushMicrotasks(); + return completer.future; + }); + }, overrides: { + MDnsVmServiceDiscovery: () => FakeMDnsVmServiceDiscovery(returnsNull: true), + }); }); }); } @@ -980,9 +1052,10 @@ IOSDevice setUpIOSDevice({ bool isCoreDevice = false, IOSCoreDeviceControl? coreDeviceControl, FakeXcodeDebug? xcodeDebug, + FakePlatform? platform, }) { final Artifacts artifacts = Artifacts.test(); - final FakePlatform macPlatform = FakePlatform( + final FakePlatform macPlatform = platform ?? FakePlatform( operatingSystem: 'macos', environment: {}, );