Unverified Commit e5c286e0 authored by Victoria Ashworth's avatar Victoria Ashworth Committed by GitHub

Upload DerivedData logs in CI (#142643)

When the Dart VM is not found within 10 minutes in CI on CoreDevices (iOS 17+), stop the app and upload the logs from DerivedData. The app has to be stopped first since the logs are not put in DerivedData until it's stopped.

Also, rearranged some logic to have CoreDevice have its own function for Dart VM url discovery.

Debugging for https://github.com/flutter/flutter/issues/142448.
parent 899f4234
......@@ -469,6 +469,7 @@ List<String> _flutterCommandArgs(String command, List<String> options) {
final String? localEngineHost = localEngineHostFromEnv;
final String? localEngineSrcPath = localEngineSrcPathFromEnv;
final String? localWebSdk = localWebSdkFromEnv;
final bool pubOrPackagesCommand = command.startsWith('packages') || command.startsWith('pub');
return <String>[
command,
if (deviceOperatingSystem == DeviceOperatingSystem.ios && supportedDeviceTimeoutCommands.contains(command))
......@@ -489,7 +490,9 @@ List<String> _flutterCommandArgs(String command, List<String> 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}'
];
}
......
......@@ -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
......
......@@ -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,
);
}
}
......
......@@ -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;
......
......@@ -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<String, Object?> 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?,
);
}
......
......@@ -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,
);
} else {
// 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 (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...',
);
});
// 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,
// 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();
}
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<Uri?> vmUrlFromMDns = MDnsVmServiceDiscovery.instance!.getVMServiceUriForLaunch(
packageId,
this,
usesIpv6: ipv6,
);
final Future<Uri?> vmUrlFromLogs = vmServiceDiscovery.uri;
localUri = await Future.any(
<Future<Uri?>>[vmUrlFromMDns, vmUrlFromLogs]
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,
);
// If the first future to return is null, wait for the other to complete.
if (localUri == null) {
final List<Uri?> vmUrls = await Future.wait(
<Future<Uri?>>[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...');
installationResult = await iosDeployDebugger!.launchAndAttach() ? 0 : 1;
if (installationResult != 0) {
_printInstallError(bundle);
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;
return LaunchResult.failed();
}
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<Uri?> _discoverDartVMForCoreDevice({
required String packageId,
required bool ipv6,
required DebuggingOptions debuggingOptions,
ProtocolDiscovery? vmServiceDiscovery,
}) async {
Timer? maxWaitForCI;
final Completer<Uri?> cancelCompleter = Completer<Uri?>();
// 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<Uri?> vmUrlFromMDns = MDnsVmServiceDiscovery.instance!.getVMServiceUriForLaunch(
packageId,
this,
usesIpv6: ipv6,
useDeviceIPAsHost: isWirelesslyConnected,
);
final List<Future<Uri?>> discoveryOptions = <Future<Uri?>>[
vmUrlFromMDns,
];
// vmServiceDiscovery uses device logs (`idevicesyslog`), which doesn't work
// on wireless devices.
if (vmServiceDiscovery != null && !isWirelesslyConnected) {
final Future<Uri?> vmUrlFromLogs = vmServiceDiscovery.uri;
discoveryOptions.add(vmUrlFromLogs);
}
Uri? localUri = await Future.any(
<Future<Uri?>>[...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<List<Uri?>> allDiscoveryOptionsComplete = Future.wait(discoveryOptions);
await Future.any(<Future<Object?>>[
allDiscoveryOptionsComplete,
cancelCompleter.future,
]);
if (!cancelCompleter.isCompleted) {
// If it wasn't cancelled, that means one of the discovery options completed.
final List<Uri?> vmUrls = await allDiscoveryOptionsComplete;
localUri = vmUrls.where((Uri? vmUrl) => vmUrl != null).firstOrNull;
}
}
maxWaitForCI?.cancel();
return localUri;
}
ProtocolDiscovery _setupDebuggerAndVmServiceDiscovery({
required IOSApp package,
required Directory bundle,
......
......@@ -377,6 +377,8 @@ abstract class FlutterCommand extends Command<void> {
/// 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.
......
......@@ -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<void> {
......@@ -164,6 +165,11 @@ class FlutterCommandRunner extends CommandRunner<void> {
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
......
......@@ -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: <Type, Generator>{
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
......
......@@ -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: <Type, Generator>{
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
......
......@@ -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: <String>['--enable-dart-profiling'],
expectedBundlePath: bundleLocation.path,
),
platform: FakePlatform(
operatingSystem: 'macos',
environment: <String, String>{
'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<void> completer = Completer<void>();
await FakeAsync().run((FakeAsync time) {
final Future<LaunchResult> futureLaunchResult = device.startApp(iosApp,
prebuiltApplication: true,
debuggingOptions: DebuggingOptions.enabled(
BuildInfo.debug,
usingCISystem: true,
debugLogsDirectoryPath: pathToFlutterLogs,
),
platformArgs: <String, dynamic>{},
);
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: <Type, Generator>{
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: <String, String>{},
);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment