Unverified Commit 0f2af976 authored by Zachary Anderson's avatar Zachary Anderson Committed by GitHub

[flutter_tools] Add a timeout to another showBuildSettings command (#39579)

parent d50d9c5e
......@@ -278,6 +278,9 @@ class BufferLogger extends Logger {
String get statusText => _status.toString();
String get traceText => _trace.toString();
@override
bool get hasTerminal => false;
@override
void printError(
String message, {
......
......@@ -32,6 +32,7 @@ import 'features.dart';
import 'fuchsia/fuchsia_device.dart' show FuchsiaDeviceTools;
import 'fuchsia/fuchsia_sdk.dart' show FuchsiaSdk, FuchsiaArtifacts;
import 'fuchsia/fuchsia_workflow.dart' show FuchsiaWorkflow;
import 'ios/devices.dart' show IOSDeploy;
import 'ios/ios_workflow.dart';
import 'ios/mac.dart';
import 'ios/simulators.dart';
......@@ -90,6 +91,7 @@ Future<T> runInContext<T>(
GenSnapshot: () => const GenSnapshot(),
HotRunnerConfig: () => HotRunnerConfig(),
IMobileDevice: () => IMobileDevice(),
IOSDeploy: () => const IOSDeploy(),
IOSSimulatorUtils: () => IOSSimulatorUtils(),
IOSWorkflow: () => const IOSWorkflow(),
KernelCompilerFactory: () => const KernelCompilerFactory(),
......
......@@ -8,6 +8,7 @@ import 'package:meta/meta.dart';
import '../application_package.dart';
import '../artifacts.dart';
import '../base/context.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
......@@ -28,6 +29,8 @@ import 'mac.dart';
class IOSDeploy {
const IOSDeploy();
static IOSDeploy get instance => context.get<IOSDeploy>();
/// Installs and runs the specified app bundle using ios-deploy, then returns
/// the exit code.
Future<int> runApp({
......@@ -365,7 +368,7 @@ class IOSDevice extends Device {
);
}
final int installationResult = await const IOSDeploy().runApp(
final int installationResult = await IOSDeploy.instance.runApp(
deviceId: id,
bundlePath: bundle.path,
launchArguments: launchArguments,
......
......@@ -462,22 +462,41 @@ Future<XcodeBuildResult> buildXcodeProject({
);
flutterUsage.sendTiming('build', 'xcode-ios', Duration(milliseconds: sw.elapsedMilliseconds));
// Run -showBuildSettings again but with the exact same parameters as the build.
final Map<String, String> buildSettings = parseXcodeBuildSettings(runCheckedSync(
(List<String>
.from(buildCommands)
..add('-showBuildSettings'))
// Undocumented behavior: xcodebuild craps out if -showBuildSettings
// is used together with -allowProvisioningUpdates or
// -allowProvisioningDeviceRegistration and freezes forever.
.where((String buildCommand) {
return !const <String>[
'-allowProvisioningUpdates',
'-allowProvisioningDeviceRegistration',
].contains(buildCommand);
}).toList(),
workingDirectory: app.project.hostAppRoot.path,
));
// Run -showBuildSettings again but with the exact same parameters as the
// build. showBuildSettings is reported to ocassionally timeout. Here, we give
// it a lot of wiggle room (locally on Flutter Gallery, this takes ~1s).
// When there is a timeout, we retry once. See issue #35988.
final List<String> showBuildSettingsCommand = (List<String>
.from(buildCommands)
..add('-showBuildSettings'))
// Undocumented behavior: xcodebuild craps out if -showBuildSettings
// is used together with -allowProvisioningUpdates or
// -allowProvisioningDeviceRegistration and freezes forever.
.where((String buildCommand) {
return !const <String>[
'-allowProvisioningUpdates',
'-allowProvisioningDeviceRegistration',
].contains(buildCommand);
}).toList();
const Duration showBuildSettingsTimeout = Duration(minutes: 1);
Map<String, String> buildSettings;
try {
final RunResult showBuildSettingsResult = await runCheckedAsync(
showBuildSettingsCommand,
workingDirectory: app.project.hostAppRoot.path,
timeout: showBuildSettingsTimeout,
timeoutRetries: 1,
);
final String showBuildSettings = showBuildSettingsResult.stdout.trim();
buildSettings = parseXcodeBuildSettings(showBuildSettings);
} on ProcessException catch (e) {
if (e.toString().contains('timed out')) {
BuildEvent('xcode-show-build-settings-timeout',
command: showBuildSettingsCommand.join(' '),
).send();
}
rethrow;
}
if (buildResult.exitCode != 0) {
printStatus('Failed to build iOS app');
......
......@@ -20,6 +20,7 @@ import '../build_info.dart';
import '../cache.dart';
import '../globals.dart';
import '../project.dart';
import '../reporting/reporting.dart';
final RegExp _settingExpr = RegExp(r'(\w+)\s*=\s*(.*)$');
final RegExp _varExpr = RegExp(r'\$\(([^)]*)\)');
......@@ -278,18 +279,20 @@ class XcodeProjectInterpreter {
final Status status = Status.withSpinner(
timeout: timeoutConfiguration.fastOperation,
);
final List<String> showBuildSettingsCommand = <String>[
_executable,
'-project',
fs.path.absolute(projectPath),
'-target',
target,
'-showBuildSettings',
];
try {
// showBuildSettings is reported to ocassionally timeout. Here, we give it
// a lot of wiggle room (locally on Flutter Gallery, this takes ~1s).
// When there is a timeout, we retry once.
final RunResult result = await runCheckedAsync(<String>[
_executable,
'-project',
fs.path.absolute(projectPath),
'-target',
target,
'-showBuildSettings',
],
final RunResult result = await runCheckedAsync(
showBuildSettingsCommand,
workingDirectory: projectPath,
timeout: timeout,
timeoutRetries: 1,
......@@ -297,6 +300,11 @@ class XcodeProjectInterpreter {
final String out = result.stdout.trim();
return parseXcodeBuildSettings(out);
} catch(error) {
if (error is ProcessException && error.toString().contains('timed out')) {
BuildEvent('xcode-show-build-settings-timeout',
command: showBuildSettingsCommand.join(' '),
).send();
}
printTrace('Unexpected failure to get the build settings: $error.');
return const <String, String>{};
} finally {
......
......@@ -103,7 +103,10 @@ void main() {
// MockProcessManager has an implementation of start() that returns the
// result of processFactory.
flakyProcessManager = MockProcessManager();
flakyProcessManager.processFactory = flakyProcessFactory(1, delay: delay);
flakyProcessManager.processFactory = flakyProcessFactory(
flakes: 1,
delay: delay,
);
});
testUsingContext('flaky process fails without retry', () async {
......
......@@ -3,7 +3,9 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'package:args/command_runner.dart';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/application_package.dart';
......@@ -12,14 +14,19 @@ import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/create.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/doctor.dart';
import 'package:flutter_tools/src/ios/devices.dart';
import 'package:flutter_tools/src/ios/mac.dart';
import 'package:flutter_tools/src/ios/ios_workflow.dart';
import 'package:flutter_tools/src/macos/xcode.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:meta/meta.dart';
import 'package:mockito/mockito.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';
import 'package:quiver/testing/async.dart';
import '../../src/common.dart';
import '../../src/context.dart';
......@@ -31,6 +38,7 @@ class MockCache extends Mock implements Cache {}
class MockDirectory extends Mock implements Directory {}
class MockFileSystem extends Mock implements FileSystem {}
class MockIMobileDevice extends Mock implements IMobileDevice {}
class MockIOSDeploy extends Mock implements IOSDeploy {}
class MockXcode extends Mock implements Xcode {}
class MockFile extends Mock implements File {}
class MockPortForwarder extends Mock implements DevicePortForwarder {}
......@@ -71,6 +79,11 @@ void main() {
MockProcessManager mockProcessManager;
MockDeviceLogReader mockLogReader;
MockPortForwarder mockPortForwarder;
MockIMobileDevice mockIMobileDevice;
MockIOSDeploy mockIosDeploy;
Directory tempDir;
Directory projectDir;
const int devicePort = 499;
const int hostPort = 42;
......@@ -86,6 +99,8 @@ void main() {
);
setUp(() {
Cache.disableLocking();
mockApp = MockIOSApp();
mockArtifacts = MockArtifacts();
mockCache = MockCache();
......@@ -94,6 +109,11 @@ void main() {
mockProcessManager = MockProcessManager();
mockLogReader = MockDeviceLogReader();
mockPortForwarder = MockPortForwarder();
mockIMobileDevice = MockIMobileDevice();
mockIosDeploy = MockIOSDeploy();
tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_create_test.');
projectDir = tempDir.childDirectory('flutter_project');
when(
mockArtifacts.getArtifactPath(
......@@ -126,10 +146,16 @@ void main() {
.thenAnswer(
(_) => Future<ProcessResult>.value(ProcessResult(1, 0, '', ''))
);
when(mockIMobileDevice.getInfoForDevice(any, 'CPUArchitecture'))
.thenAnswer((_) => Future<String>.value('arm64'));
});
tearDown(() {
mockLogReader.dispose();
tryToDelete(tempDir);
Cache.enableLocking();
});
testUsingContext(' succeeds in debug mode', () async {
......@@ -202,6 +228,91 @@ void main() {
Platform: () => macPlatform,
ProcessManager: () => mockProcessManager,
});
void testNonPrebuilt({
@required bool showBuildSettingsFlakes,
}) {
const String name = ' non-prebuilt succeeds in debug mode';
testUsingContext(name + ' flaky: $showBuildSettingsFlakes', () async {
final Directory targetBuildDir =
projectDir.childDirectory('build/ios/iphoneos/Debug-arm64');
// The -showBuildSettings calls have a timeout and so go through
// processManager.start().
mockProcessManager.processFactory = flakyProcessFactory(
flakes: showBuildSettingsFlakes ? 1 : 0,
delay: const Duration(seconds: 62),
filter: (List<String> args) => args.contains('-showBuildSettings'),
stdout:
() => Stream<String>
.fromIterable(
<String>['TARGET_BUILD_DIR = ${targetBuildDir.path}\n'])
.transform(utf8.encoder),
);
// Make all other subcommands succeed.
when(mockProcessManager.run(
any,
workingDirectory: anyNamed('workingDirectory'),
environment: anyNamed('environment'),
)).thenAnswer((Invocation inv) {
return Future<ProcessResult>.value(ProcessResult(0, 0, '', ''));
});
// Deploy works.
when(mockIosDeploy.runApp(
deviceId: anyNamed('deviceId'),
bundlePath: anyNamed('bundlePath'),
launchArguments: anyNamed('launchArguments'),
)).thenAnswer((_) => Future<int>.value(0));
// Create a dummy project to avoid mocking out the whole directory
// structure expected by device.startApp().
Cache.flutterRoot = '../..';
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>[
'create',
'--no-pub',
projectDir.path,
]);
final IOSApp app =
AbsoluteBuildableIOSApp(FlutterProject.fromDirectory(projectDir).ios);
final IOSDevice device = IOSDevice('123');
// Pre-create the expected build products.
targetBuildDir.createSync(recursive: true);
projectDir.childDirectory('build/ios/iphoneos/Runner.app').createSync(recursive: true);
final Completer<LaunchResult> completer = Completer<LaunchResult>();
FakeAsync().run((FakeAsync time) {
device.startApp(
app,
prebuiltApplication: false,
debuggingOptions: DebuggingOptions.disabled(const BuildInfo(BuildMode.debug, null)),
platformArgs: <String, dynamic>{},
).then((LaunchResult result) {
completer.complete(result);
});
time.flushMicrotasks();
time.elapse(const Duration(seconds: 65));
});
final LaunchResult launchResult = await completer.future;
expect(launchResult.started, isTrue);
expect(launchResult.hasObservatory, isFalse);
expect(await device.stopApp(mockApp), isFalse);
}, overrides: <Type, Generator>{
DoctorValidatorsProvider: () => FakeIosDoctorProvider(),
IMobileDevice: () => mockIMobileDevice,
IOSDeploy: () => mockIosDeploy,
Platform: () => macPlatform,
ProcessManager: () => mockProcessManager,
});
}
testNonPrebuilt(showBuildSettingsFlakes: false);
testNonPrebuilt(showBuildSettingsFlakes: true);
});
group('Process calls', () {
......@@ -499,3 +610,29 @@ flutter:
Platform: () => macPlatform,
});
}
class AbsoluteBuildableIOSApp extends BuildableIOSApp {
AbsoluteBuildableIOSApp(IosProject project) : super(project);
@override
String get deviceBundlePath =>
fs.path.join(project.parent.directory.path, 'build', 'ios', 'iphoneos', name);
}
class FakeIosDoctorProvider implements DoctorValidatorsProvider {
List<Workflow> _workflows;
@override
List<DoctorValidator> get validators => <DoctorValidator>[];
@override
List<Workflow> get workflows {
if (_workflows == null) {
_workflows = <Workflow>[];
if (iosWorkflow.appliesToHostPlatform)
_workflows.add(iosWorkflow);
}
return _workflows;
}
}
......@@ -153,8 +153,10 @@ void main() {
testUsingContext('build settings flakes', () async {
const Duration delay = Duration(seconds: 1);
mockProcessManager.processFactory =
mocks.flakyProcessFactory(1, delay: delay + const Duration(seconds: 1));
mockProcessManager.processFactory = mocks.flakyProcessFactory(
flakes: 1,
delay: delay + const Duration(seconds: 1),
);
expect(await xcodeProjectInterpreter.getBuildSettingsAsync(
'', '', timeout: delay),
const <String, String>{});
......
......@@ -185,11 +185,26 @@ class MockProcessManager extends Mock implements ProcessManager {
/// A function that generates a process factory that gives processes that fail
/// a given number of times before succeeding. The returned processes will
/// fail after a delay if one is supplied.
ProcessFactory flakyProcessFactory(int flakes, {Duration delay}) {
ProcessFactory flakyProcessFactory({
int flakes,
bool Function(List<String> command) filter,
Duration delay,
Stream<List<int>> Function() stdout,
Stream<List<int>> Function() stderr,
}) {
int flakesLeft = flakes;
stdout ??= () => const Stream<List<int>>.empty();
stderr ??= () => const Stream<List<int>>.empty();
return (List<String> command) {
if (filter != null && !filter(command)) {
return MockProcess();
}
if (flakesLeft == 0) {
return MockProcess(exitCode: Future<int>.value(0));
return MockProcess(
exitCode: Future<int>.value(0),
stdout: stdout(),
stderr: stderr(),
);
}
flakesLeft = flakesLeft - 1;
Future<int> exitFuture;
......@@ -198,7 +213,11 @@ ProcessFactory flakyProcessFactory(int flakes, {Duration delay}) {
} else {
exitFuture = Future<int>.delayed(delay, () => Future<int>.value(-9));
}
return MockProcess(exitCode: exitFuture);
return MockProcess(
exitCode: exitFuture,
stdout: stdout(),
stderr: stderr(),
);
};
}
......
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