Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Retry connecting to device in CI after lost connection #133769

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
109 changes: 78 additions & 31 deletions packages/flutter_tools/lib/src/ios/devices.dart
Original file line number Diff line number Diff line change
Expand Up @@ -537,34 +537,12 @@ class IOSDevice extends Device {
int installationResult = 1;
if (debuggingOptions.debuggingEnabled) {
_logger.printTrace('Debugging is enabled, connecting to vmService');
final DeviceLogReader deviceLogReader = getLogReader(
app: package,
usingCISystem: debuggingOptions.usingCISystem,
);

// If the device supports syslog reading, prefer launching the app without
// attaching the debugger to avoid the overhead of the unnecessary extra running process.
if (majorSdkVersion >= IOSDeviceLogReader.minimumUniversalLoggingSdkVersion) {
iosDeployDebugger = _iosDeploy.prepareDebuggerForLaunch(
deviceId: id,
bundlePath: bundle.path,
appDeltaDirectory: package.appDeltaDirectory,
launchArguments: launchArguments,
interfaceType: connectionInterface,
uninstallFirst: debuggingOptions.uninstallFirst,
);
if (deviceLogReader is IOSDeviceLogReader) {
deviceLogReader.debuggerStream = iosDeployDebugger;
}
}
// Don't port foward if debugging with a wireless device.
vmServiceDiscovery = ProtocolDiscovery.vmService(
deviceLogReader,
portForwarder: isWirelesslyConnected ? null : portForwarder,
hostPort: debuggingOptions.hostVmServicePort,
devicePort: debuggingOptions.deviceVmServicePort,
vmServiceDiscovery = _setupDebuggerAndVmServiceDiscovery(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from a cursory look, this diff doesn't change anything, just factors out the left side logic to a helper method, is that right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty much, only difference is I added a skipInstall option

package: package,
bundle: bundle,
debuggingOptions: debuggingOptions,
launchArguments: launchArguments,
ipv6: ipv6,
logger: _logger,
);
}

Expand All @@ -589,10 +567,7 @@ class IOSDevice extends Device {
installationResult = await iosDeployDebugger!.launchAndAttach() ? 0 : 1;
}
if (installationResult != 0) {
_logger.printError('Could not run ${bundle.path} on $id.');
_logger.printError('Try launching Xcode and selecting "Product > Run" to fix the problem:');
_logger.printError(' open ios/Runner.xcworkspace');
_logger.printError('');
_printInstallError(bundle);
await dispose();
return LaunchResult.failed();
}
Expand Down Expand Up @@ -704,6 +679,31 @@ class IOSDevice extends Device {
);
} 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,
skipInstall: true,
);
installationResult = await iosDeployDebugger!.launchAndAttach() ? 0 : 1;
if (installationResult != 0) {
_printInstallError(bundle);
await dispose();
return LaunchResult.failed();
}
localUri = await vmServiceDiscovery.uri;
}
}
}
timer.cancel();
Expand All @@ -723,6 +723,53 @@ class IOSDevice extends Device {
}
}

void _printInstallError(Directory bundle) {
_logger.printError('Could not run ${bundle.path} on $id.');
_logger.printError('Try launching Xcode and selecting "Product > Run" to fix the problem:');
_logger.printError(' open ios/Runner.xcworkspace');
_logger.printError('');
}

ProtocolDiscovery _setupDebuggerAndVmServiceDiscovery({
required IOSApp package,
required Directory bundle,
required DebuggingOptions debuggingOptions,
required List<String> launchArguments,
required bool ipv6,
bool skipInstall = false,
}) {
final DeviceLogReader deviceLogReader = getLogReader(
app: package,
usingCISystem: debuggingOptions.usingCISystem,
);

// If the device supports syslog reading, prefer launching the app without
// attaching the debugger to avoid the overhead of the unnecessary extra running process.
if (majorSdkVersion >= IOSDeviceLogReader.minimumUniversalLoggingSdkVersion) {
iosDeployDebugger = _iosDeploy.prepareDebuggerForLaunch(
deviceId: id,
bundlePath: bundle.path,
appDeltaDirectory: package.appDeltaDirectory,
launchArguments: launchArguments,
interfaceType: connectionInterface,
uninstallFirst: debuggingOptions.uninstallFirst,
skipInstall: skipInstall,
);
if (deviceLogReader is IOSDeviceLogReader) {
deviceLogReader.debuggerStream = iosDeployDebugger;
}
}
// Don't port foward if debugging with a wireless device.
return ProtocolDiscovery.vmService(
deviceLogReader,
portForwarder: isWirelesslyConnected ? null : portForwarder,
hostPort: debuggingOptions.hostVmServicePort,
devicePort: debuggingOptions.deviceVmServicePort,
ipv6: ipv6,
logger: _logger,
);
}

/// Starting with Xcode 15 and iOS 17, `ios-deploy` stopped working due to
/// the new CoreDevice connectivity stack. Previously, `ios-deploy` was used
/// to install the app, launch the app, and start `debugserver`.
Expand Down
14 changes: 14 additions & 0 deletions packages/flutter_tools/lib/src/ios/ios_deploy.dart
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ class IOSDeploy {
required DeviceConnectionInterface interfaceType,
Directory? appDeltaDirectory,
required bool uninstallFirst,
bool skipInstall = false,
}) {
appDeltaDirectory?.createSync(recursive: true);
// Interactive debug session to support sending the lldb detach command.
Expand All @@ -148,6 +149,8 @@ class IOSDeploy {
],
if (uninstallFirst)
'--uninstall',
if (skipInstall)
'--noinstall',
'--debug',
if (interfaceType != DeviceConnectionInterface.wireless)
'--no-wifi',
Expand Down Expand Up @@ -327,6 +330,14 @@ class IOSDeployDebugger {
/// The future should be completed once the backtraces are logged.
Completer<void>? _processResumeCompleter;

// Process 525 exited with status = -1 (0xffffffff) lost connection
static final RegExp _lostConnectionPattern = RegExp(r'exited with status = -1 \(0xffffffff\) lost connection');

/// Whether ios-deploy received a message matching [_lostConnectionPattern],
/// indicating that it lost connection to the device.
bool get lostConnection => _lostConnection;
bool _lostConnection = false;

/// Launch the app on the device, and attach the debugger.
///
/// Returns whether or not the debugger successfully attached.
Expand Down Expand Up @@ -448,6 +459,9 @@ class IOSDeployDebugger {
// The app exited or crashed, so exit. Continue passing debugging
// messages to the log reader until it exits to capture crash dumps.
_logger.printTrace(line);
if (line.contains(_lostConnectionPattern)) {
_lostConnection = true;
}
exit();
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ FakeCommand attachDebuggerCommand({
String stdout = '(lldb) run\nsuccess',
Completer<void>? completer,
bool isWirelessDevice = false,
bool skipInstall = false,
}) {
return FakeCommand(
command: <String>[
Expand All @@ -87,6 +88,8 @@ FakeCommand attachDebuggerCommand({
'123',
'--bundle',
'/',
if (skipInstall)
'--noinstall',
'--debug',
if (!isWirelessDevice) '--no-wifi',
'--args',
Expand Down Expand Up @@ -339,6 +342,86 @@ void main() {
MDnsVmServiceDiscovery: () => FakeMDnsVmServiceDiscovery(),
});

testWithoutContext('IOSDevice.startApp retries when ios-deploy loses connection the first time in CI', () async {
final BufferLogger logger = BufferLogger.test();
final FileSystem fileSystem = MemoryFileSystem.test();
final Completer<void> completer = Completer<void>();
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
attachDebuggerCommand(
stdout: '(lldb) run\nsuccess\nProcess 525 exited with status = -1 (0xffffffff) lost connection',
),
attachDebuggerCommand(
stdout: '(lldb) run\nsuccess\nThe Dart VM service is listening on http://127.0.0.1:456',
completer: completer,
skipInstall: true,
),
]);
final IOSDevice device = setUpIOSDevice(
processManager: processManager,
fileSystem: fileSystem,
logger: logger,
);
final IOSApp iosApp = PrebuiltIOSApp(
projectBundleId: 'app',
bundleName: 'Runner',
uncompressedBundle: fileSystem.currentDirectory,
applicationPackage: fileSystem.currentDirectory,
);

device.portForwarder = const NoOpDevicePortForwarder();

final LaunchResult launchResult = await device.startApp(iosApp,
prebuiltApplication: true,
debuggingOptions: DebuggingOptions.enabled(
BuildInfo.debug,
usingCISystem: true,
),
platformArgs: <String, dynamic>{},
);
completer.complete();

expect(processManager, hasNoRemainingExpectations);
expect(launchResult.started, true);
expect(launchResult.hasVmService, true);
expect(await device.stopApp(iosApp), false);
});

testWithoutContext('IOSDevice.startApp does not retry when ios-deploy loses connection if not in CI', () async {
final BufferLogger logger = BufferLogger.test();
final FileSystem fileSystem = MemoryFileSystem.test();
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
attachDebuggerCommand(
stdout: '(lldb) run\nsuccess\nProcess 525 exited with status = -1 (0xffffffff) lost connection',
),
]);
final IOSDevice device = setUpIOSDevice(
processManager: processManager,
fileSystem: fileSystem,
logger: logger,
);
final IOSApp iosApp = PrebuiltIOSApp(
projectBundleId: 'app',
bundleName: 'Runner',
uncompressedBundle: fileSystem.currentDirectory,
applicationPackage: fileSystem.currentDirectory,
);

device.portForwarder = const NoOpDevicePortForwarder();

final LaunchResult launchResult = await device.startApp(iosApp,
prebuiltApplication: true,
debuggingOptions: DebuggingOptions.enabled(
BuildInfo.debug,
),
platformArgs: <String, dynamic>{},
);

expect(processManager, hasNoRemainingExpectations);
expect(launchResult.started, false);
expect(launchResult.hasVmService, false);
expect(await device.stopApp(iosApp), false);
});

testWithoutContext('IOSDevice.startApp succeeds in release mode', () async {
final FileSystem fileSystem = MemoryFileSystem.test();
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
Expand Down