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

Workaround for Dart VM timeout (#127875)

Workaround solution for: https://github.com/flutter/flutter/issues/121231
See https://github.com/flutter/flutter/issues/120808#issuecomment-1551826299 Error Case 2 for more information.

Sometimes the `ios-deploy` process does not return the logs from the application. We've been unable to figure out why. This is a solution to workaround that by using `idevicesyslog` alongside `ios-deploy` as a backup in getting the log for the Dart VM url. As explained in https://github.com/flutter/flutter/issues/120808#issuecomment-1551826299, when error case 2 happens, the `idevicesyslog` does successfully find the Dart VM.

Also, in the comments of the code it mentions `syslog` is not written on iOS 13+, this was added in response to this issue: https://github.com/flutter/flutter/issues/41133.

However, `idevicesyslog` does in fact work (at least for iOS 16), we use it to collect device logs for our CI tests already: https://github.com/flutter/flutter/blob/1dc26f80f04c1c1091a83c5fead9b3aa83483292/dev/devicelab/lib/framework/devices.dart#L998-L1006
parent 95cd3c03
......@@ -471,6 +471,10 @@ List<String> _flutterCommandArgs(String command, List<String> options) {
if (localEngineSrcPath != null) ...<String>['--local-engine-src-path', localEngineSrcPath],
if (localWebSdk != null) ...<String>['--local-web-sdk', localWebSdk],
...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',
];
}
......
......@@ -33,6 +33,7 @@ import '../resident_runner.dart';
import '../run_cold.dart';
import '../run_hot.dart';
import '../runner/flutter_command.dart';
import '../runner/flutter_command_runner.dart';
import '../vmservice.dart';
/// A Flutter-command that attaches to applications that have been launched
......@@ -528,6 +529,7 @@ known, it can be explicitly provided to attach via the command-line, e.g.
ddsPort: ddsPort,
devToolsServerAddress: devToolsServerAddress,
serveObservatory: serveObservatory,
usingCISystem: usingCISystem,
);
return buildInfo.isDebug
......@@ -535,7 +537,7 @@ known, it can be explicitly provided to attach via the command-line, e.g.
flutterDevices,
target: targetFile,
debuggingOptions: debuggingOptions,
packagesFilePath: globalResults!['packages'] as String?,
packagesFilePath: globalResults![FlutterGlobalOptions.kPackagesOption] as String?,
projectRootPath: stringArg('project-root'),
dillOutputPath: stringArg('output-dill'),
ipv6: usesIpv6,
......
......@@ -6,6 +6,7 @@ import '../base/common.dart';
import '../cache.dart';
import '../globals.dart' as globals;
import '../runner/flutter_command.dart';
import '../runner/flutter_command_runner.dart';
import '../version.dart';
class ChannelCommand extends FlutterCommand {
......@@ -40,7 +41,7 @@ class ChannelCommand extends FlutterCommand {
case 0:
await _listChannels(
showAll: boolArg('all'),
verbose: globalResults?['verbose'] == true,
verbose: globalResults?[FlutterGlobalOptions.kVerboseFlag] == true,
);
return FlutterCommandResult.success();
case 1:
......
......@@ -25,6 +25,7 @@ import '../custom_devices/custom_devices_config.dart';
import '../device_port_forwarder.dart';
import '../features.dart';
import '../runner/flutter_command.dart';
import '../runner/flutter_command_runner.dart';
/// just the function signature of the [print] function.
/// The Object arg may be null.
......@@ -811,7 +812,7 @@ Delete a device from the config file.
Future<FlutterCommandResult> runCommand() async {
checkFeatureEnabled();
final String? id = globalResults!['device-id'] as String?;
final String? id = globalResults![FlutterGlobalOptions.kDeviceIdOption] as String?;
if (id == null || !customDevicesConfig.contains(id)) {
throwToolExit('Couldn\'t find device with id "$id" in config at "${customDevicesConfig.configPath}"');
}
......
......@@ -26,6 +26,7 @@ import '../resident_runner.dart';
import '../run_cold.dart';
import '../run_hot.dart';
import '../runner/flutter_command.dart';
import '../runner/flutter_command_runner.dart';
import '../tracing.dart';
import '../vmservice.dart';
import '../web/web_runner.dart';
......@@ -247,6 +248,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment
uninstallFirst: uninstallFirst,
enableDartProfiling: enableDartProfiling,
enableEmbedderApi: enableEmbedderApi,
usingCISystem: usingCISystem,
);
} else {
return DebuggingOptions.enabled(
......@@ -298,6 +300,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment
serveObservatory: boolArg('serve-observatory'),
enableDartProfiling: enableDartProfiling,
enableEmbedderApi: enableEmbedderApi,
usingCISystem: usingCISystem,
);
}
}
......@@ -643,7 +646,7 @@ class RunCommand extends RunCommandBase {
: globals.fs.file(applicationBinaryPath),
trackWidgetCreation: trackWidgetCreation,
projectRootPath: stringArg('project-root'),
packagesFilePath: globalResults!['packages'] as String?,
packagesFilePath: globalResults![FlutterGlobalOptions.kPackagesOption] as String?,
dillOutputPath: stringArg('output-dill'),
ipv6: ipv6 ?? false,
multidexEnabled: boolArg('multidex'),
......
......@@ -422,6 +422,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
disablePortPublication: true,
enableDds: enableDds,
nullAssertions: boolArg(FlutterOptions.kNullAssertions),
usingCISystem: usingCISystem,
);
Device? integrationTestDevice;
......
......@@ -971,6 +971,7 @@ class DebuggingOptions {
this.serveObservatory = false,
this.enableDartProfiling = true,
this.enableEmbedderApi = false,
this.usingCISystem = false,
}) : debuggingEnabled = true;
DebuggingOptions.disabled(this.buildInfo, {
......@@ -993,6 +994,7 @@ class DebuggingOptions {
this.uninstallFirst = false,
this.enableDartProfiling = true,
this.enableEmbedderApi = false,
this.usingCISystem = false,
}) : debuggingEnabled = false,
useTestFonts = false,
startPaused = false,
......@@ -1069,6 +1071,7 @@ class DebuggingOptions {
required this.serveObservatory,
required this.enableDartProfiling,
required this.enableEmbedderApi,
required this.usingCISystem,
});
final bool debuggingEnabled;
......@@ -1109,6 +1112,7 @@ class DebuggingOptions {
final bool serveObservatory;
final bool enableDartProfiling;
final bool enableEmbedderApi;
final bool usingCISystem;
/// Whether the tool should try to uninstall a previously installed version of the app.
///
......@@ -1243,6 +1247,7 @@ class DebuggingOptions {
'serveObservatory': serveObservatory,
'enableDartProfiling': enableDartProfiling,
'enableEmbedderApi': enableEmbedderApi,
'usingCISystem': usingCISystem,
};
static DebuggingOptions fromJson(Map<String, Object?> json, BuildInfo buildInfo) =>
......@@ -1294,6 +1299,7 @@ class DebuggingOptions {
serveObservatory: (json['serveObservatory'] as bool?) ?? false,
enableDartProfiling: (json['enableDartProfiling'] as bool?) ?? true,
enableEmbedderApi: (json['enableEmbedderApi'] as bool?) ?? false,
usingCISystem: (json['usingCISystem'] as bool?) ?? false,
);
}
......
......@@ -482,7 +482,10 @@ class IOSDevice extends Device {
int installationResult = 1;
if (debuggingOptions.debuggingEnabled) {
_logger.printTrace('Debugging is enabled, connecting to vmService');
final DeviceLogReader deviceLogReader = getLogReader(app: package);
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.
......@@ -629,12 +632,14 @@ class IOSDevice extends Device {
DeviceLogReader getLogReader({
covariant IOSApp? app,
bool includePastLogs = false,
bool usingCISystem = false,
}) {
assert(!includePastLogs, 'Past log reading not supported on iOS devices.');
return _logReaders.putIfAbsent(app, () => IOSDeviceLogReader.create(
device: this,
app: app,
iMobileDevice: _iMobileDevice,
usingCISystem: usingCISystem,
));
}
......@@ -749,17 +754,20 @@ class IOSDeviceLogReader extends DeviceLogReader {
this._deviceId,
this.name,
String appName,
bool usingCISystem,
) : // Match for lines for the runner in syslog.
//
// iOS 9 format: Runner[297] <Notice>:
// iOS 10 format: Runner(Flutter)[297] <Notice>:
_runnerLineRegex = RegExp(appName + r'(\(Flutter\))?\[[\d]+\] <[A-Za-z]+>: ');
_runnerLineRegex = RegExp(appName + r'(\(Flutter\))?\[[\d]+\] <[A-Za-z]+>: '),
_usingCISystem = usingCISystem;
/// Create a new [IOSDeviceLogReader].
factory IOSDeviceLogReader.create({
required IOSDevice device,
IOSApp? app,
required IMobileDevice iMobileDevice,
bool usingCISystem = false,
}) {
final String appName = app?.name?.replaceAll('.app', '') ?? '';
return IOSDeviceLogReader._(
......@@ -768,6 +776,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
device.id,
device.name,
appName,
usingCISystem,
);
}
......@@ -775,9 +784,17 @@ class IOSDeviceLogReader extends DeviceLogReader {
factory IOSDeviceLogReader.test({
required IMobileDevice iMobileDevice,
bool useSyslog = true,
bool usingCISystem = false,
int? majorSdkVersion,
}) {
final int sdkVersion;
if (majorSdkVersion != null) {
sdkVersion = majorSdkVersion;
} else {
sdkVersion = useSyslog ? 12 : 13;
}
return IOSDeviceLogReader._(
iMobileDevice, useSyslog ? 12 : 13, '1234', 'test', 'Runner');
iMobileDevice, sdkVersion, '1234', 'test', 'Runner', usingCISystem);
}
@override
......@@ -785,6 +802,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
final int _majorSdkVersion;
final String _deviceId;
final IMobileDevice _iMobileDevice;
final bool _usingCISystem;
// Matches a syslog line from the runner.
RegExp _runnerLineRegex;
......@@ -810,12 +828,42 @@ 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) {
void _addToLinesController(String message, IOSDeviceLogSource source) {
if (!linesController.isClosed) {
if (_excludeLog(message, source)) {
return;
}
linesController.add(message);
}
}
/// Used to track messages prefixed with "flutter:" when [useBothLogDeviceReaders]
/// is true.
final List<String> _streamFlutterMessages = <String>[];
/// When using both `idevicesyslog` and `ios-deploy`, exclude logs with the
/// "flutter:" prefix if they have already been added to the stream. This is
/// to prevent duplicates from being printed.
///
/// If a message does not have the prefix, exclude it if the message's
/// source is `idevicesyslog`. This is done because `ios-deploy` and
/// `idevicesyslog` often have different prefixes on non-flutter messages
/// and are often not critical for CI tests.
bool _excludeLog(String message, IOSDeviceLogSource source) {
if (!useBothLogDeviceReaders) {
return false;
}
if (message.startsWith('flutter:')) {
if (_streamFlutterMessages.contains(message)) {
return true;
}
_streamFlutterMessages.add(message);
} else if (source == IOSDeviceLogSource.idevicesyslog) {
return true;
}
return false;
}
final List<StreamSubscription<void>> _loggingSubscriptions = <StreamSubscription<void>>[];
@override
......@@ -835,6 +883,10 @@ class IOSDeviceLogReader extends DeviceLogReader {
static const int minimumUniversalLoggingSdkVersion = 13;
/// 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<void> _listenToUnifiedLoggingEvents(FlutterVmService connectedVmService) async {
if (_majorSdkVersion < minimumUniversalLoggingSdkVersion) {
return;
......@@ -859,7 +911,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
}
final String message = processVmServiceMessage(event);
if (message.isNotEmpty) {
_addToLinesController(message);
_addToLinesController(message, IOSDeviceLogSource.unifiedLogging);
}
}
......@@ -871,8 +923,10 @@ class IOSDeviceLogReader extends DeviceLogReader {
/// Log reader will listen to [debugger.logLines] and will detach debugger on dispose.
IOSDeployDebugger? get debuggerStream => _iosDeployDebugger;
/// Send messages from ios-deploy debugger stream to device log reader stream.
set debuggerStream(IOSDeployDebugger? debugger) {
// Logging is gathered from syslog on iOS 13 and earlier.
// Logging is gathered from syslog on iOS earlier than 13.
if (_majorSdkVersion < minimumUniversalLoggingSdkVersion) {
return;
}
......@@ -882,7 +936,10 @@ class IOSDeviceLogReader extends DeviceLogReader {
}
// Add the debugger logs to the controller created on initialization.
_loggingSubscriptions.add(debugger.logLines.listen(
(String line) => _addToLinesController(_debuggerLineHandler(line)),
(String line) => _addToLinesController(
_debuggerLineHandler(line),
IOSDeviceLogSource.iosDeploy,
),
onError: linesController.addError,
onDone: linesController.close,
cancelOnError: true,
......@@ -893,18 +950,38 @@ class IOSDeviceLogReader extends DeviceLogReader {
// Strip off the logging metadata (leave the category), or just echo the line.
String _debuggerLineHandler(String line) => _debuggerLoggingRegex.firstMatch(line)?.group(1) ?? line;
/// Use both logs from `idevicesyslog` and `ios-deploy` when debugging from CI system
/// since sometimes `ios-deploy` does not return the device logs:
/// https://github.com/flutter/flutter/issues/121231
@visibleForTesting
bool get useBothLogDeviceReaders {
return _usingCISystem && _majorSdkVersion >= 16;
}
/// Start and listen to idevicesyslog to get device logs for iOS versions
/// prior to 13 or if [useBothLogDeviceReaders] is true.
void _listenToSysLog() {
// syslog is not written on iOS 13+.
if (_majorSdkVersion >= minimumUniversalLoggingSdkVersion) {
// 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 so only use syslogs for iOS versions prior
// to 13 unless [useBothLogDeviceReaders] is true.
if (!useBothLogDeviceReaders && _majorSdkVersion >= minimumUniversalLoggingSdkVersion) {
return;
}
_iMobileDevice.startLogger(_deviceId).then<void>((Process process) {
process.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_newSyslogLineHandler());
process.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_newSyslogLineHandler());
process.exitCode.whenComplete(() {
if (linesController.hasListener) {
linesController.close();
if (!linesController.hasListener) {
return;
}
// When using both log readers, do not close the stream on exit.
// This is to allow ios-deploy to be the source of authority to close
// the stream.
if (useBothLogDeviceReaders && debuggerStream != null) {
return;
}
linesController.close();
});
assert(idevicesyslogProcess == null);
idevicesyslogProcess = process;
......@@ -926,7 +1003,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
return (String line) {
if (printing) {
if (!_anyLineRegex.hasMatch(line)) {
_addToLinesController(decodeSyslog(line));
_addToLinesController(decodeSyslog(line), IOSDeviceLogSource.idevicesyslog);
return;
}
......@@ -938,8 +1015,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));
_addToLinesController(decodeSyslog(logLine), IOSDeviceLogSource.idevicesyslog);
printing = true;
}
};
......@@ -955,6 +1031,15 @@ class IOSDeviceLogReader extends DeviceLogReader {
}
}
enum IOSDeviceLogSource {
/// Gets logs from ios-deploy debugger.
iosDeploy,
/// Gets logs from idevicesyslog.
idevicesyslog,
/// Gets logs from the Dart VM Service.
unifiedLogging,
}
/// A [DevicePortForwarder] specialized for iOS usage with iproxy.
class IOSDevicePortForwarder extends DevicePortForwarder {
......
......@@ -36,6 +36,8 @@ import 'devfs.dart';
import 'device.dart';
import 'features.dart';
import 'globals.dart' as globals;
import 'ios/application_package.dart';
import 'ios/devices.dart';
import 'project.dart';
import 'resident_devtools_handler.dart';
import 'run_cold.dart';
......@@ -391,11 +393,19 @@ class FlutterDevice {
return devFS!.create();
}
Future<void> startEchoingDeviceLog() async {
Future<void> startEchoingDeviceLog(DebuggingOptions debuggingOptions) async {
if (_loggingSubscription != null) {
return;
}
final Stream<String> logStream = (await device!.getLogReader(app: package)).logLines;
final Stream<String> logStream;
if (device is IOSDevice) {
logStream = (device! as IOSDevice).getLogReader(
app: package as IOSApp?,
usingCISystem: debuggingOptions.usingCISystem,
).logLines;
} else {
logStream = (await device!.getLogReader(app: package)).logLines;
}
_loggingSubscription = logStream.listen((String line) {
if (!line.contains(globals.kVMServiceMessageRegExp)) {
globals.printStatus(line, wrap: false);
......@@ -451,7 +461,7 @@ class FlutterDevice {
'multidex': hotRunner.multidexEnabled,
};
await startEchoingDeviceLog();
await startEchoingDeviceLog(hotRunner.debuggingOptions);
// Start the application.
final Future<LaunchResult> futureResult = device!.startApp(
......@@ -519,7 +529,7 @@ class FlutterDevice {
platformArgs['trace-startup'] = coldRunner.traceStartup;
platformArgs['multidex'] = coldRunner.multidexEnabled;
await startEchoingDeviceLog();
await startEchoingDeviceLog(coldRunner.debuggingOptions);
final LaunchResult result = await device!.startApp(
applicationPackage,
......
......@@ -304,7 +304,10 @@ abstract class FlutterCommand extends Command<void> {
/// Path to the Dart's package config file.
///
/// This can be overridden by some of its subclasses.
String? get packagesPath => globalResults?['packages'] as String?;
String? get packagesPath => stringArg(FlutterGlobalOptions.kPackagesOption, global: true);
/// Whether flutter is being run from our CI.
bool get usingCISystem => boolArg(FlutterGlobalOptions.kContinuousIntegrationFlag, global: true);
/// The value of the `--filesystem-scheme` argument.
///
......@@ -1634,17 +1637,30 @@ Run 'flutter -h' (or 'flutter <command> -h') for available flutter commands and
///
/// If no flag named [name] was added to the [ArgParser], an [ArgumentError]
/// will be thrown.
bool boolArg(String name) => argResults![name] as bool;
bool boolArg(String name, {bool global = false}) {
if (global) {
return globalResults![name] as bool;
}
return argResults![name] as bool;
}
/// Gets the parsed command-line option named [name] as a `String`.
///
/// If no option named [name] was added to the [ArgParser], an [ArgumentError]
/// will be thrown.
String? stringArg(String name) => argResults![name] as String?;
String? stringArg(String name, {bool global = false}) {
if (global) {
return globalResults![name] as String?;
}
return argResults![name] as String?;
}
/// Gets the parsed command-line option named [name] as `List<String>`.
List<String> stringsArg(String name) {
return argResults![name]! as List<String>? ?? <String>[];
List<String> stringsArg(String name, {bool global = false}) {
if (global) {
return globalResults![name] as List<String>;
}
return argResults![name] as List<String>;
}
}
......
......@@ -20,6 +20,30 @@ import '../globals.dart' as globals;
import '../tester/flutter_tester.dart';
import '../web/web_device.dart';
/// Common flutter command line options.
abstract final class FlutterGlobalOptions {
static const String kColorFlag = 'color';
static const String kContinuousIntegrationFlag = 'ci';
static const String kDeviceIdOption = 'device-id';
static const String kDisableTelemetryFlag = 'disable-telemetry';
static const String kEnableTelemetryFlag = 'enable-telemetry';
static const String kLocalEngineOption = 'local-engine';
static const String kLocalEngineSrcPathOption = 'local-engine-src-path';
static const String kLocalWebSDKOption = 'local-web-sdk';
static const String kMachineFlag = 'machine';
static const String kPackagesOption = 'packages';
static const String kPrefixedErrorsFlag = 'prefixed-errors';
static const String kQuietFlag = 'quiet';
static const String kShowTestDeviceFlag = 'show-test-device';
static const String kShowWebServerDeviceFlag = 'show-web-server-device';
static const String kSuppressAnalyticsFlag = 'suppress-analytics';
static const String kVerboseFlag = 'verbose';
static const String kVersionCheckFlag = 'version-check';
static const String kVersionFlag = 'version';
static const String kWrapColumnOption = 'wrap-column';
static const String kWrapFlag = 'wrap';
}
class FlutterCommandRunner extends CommandRunner<void> {
FlutterCommandRunner({ bool verboseHelp = false }) : super(
'flutter',
......@@ -33,80 +57,80 @@ class FlutterCommandRunner extends CommandRunner<void> {
' flutter run [options]\n'
' Run your Flutter application on an attached device or in an emulator.',
) {
argParser.addFlag('verbose',
argParser.addFlag(FlutterGlobalOptions.kVerboseFlag,
abbr: 'v',
negatable: false,
help: 'Noisy logging, including all shell commands executed.\n'
'If used with "--help", shows hidden options. '
'If used with "flutter doctor", shows additional diagnostic information. '
'(Use "-vv" to force verbose logging in those cases.)');
argParser.addFlag('prefixed-errors',
argParser.addFlag(FlutterGlobalOptions.kPrefixedErrorsFlag,
negatable: false,
help: 'Causes lines sent to stderr to be prefixed with "ERROR:".',
hide: !verboseHelp);
argParser.addFlag('quiet',
argParser.addFlag(FlutterGlobalOptions.kQuietFlag,
negatable: false,
hide: !verboseHelp,
help: 'Reduce the amount of output from some commands.');
argParser.addFlag('wrap',
argParser.addFlag(FlutterGlobalOptions.kWrapFlag,
hide: !verboseHelp,
help: 'Toggles output word wrapping, regardless of whether or not the output is a terminal.',
defaultsTo: true);
argParser.addOption('wrap-column',
argParser.addOption(FlutterGlobalOptions.kWrapColumnOption,
hide: !verboseHelp,
help: 'Sets the output wrap column. If not set, uses the width of the terminal. No '
'wrapping occurs if not writing to a terminal. Use "--no-wrap" to turn off wrapping '
'when connected to a terminal.');
argParser.addOption('device-id',
argParser.addOption(FlutterGlobalOptions.kDeviceIdOption,
abbr: 'd',
help: 'Target device id or name (prefixes allowed).');
argParser.addFlag('version',
argParser.addFlag(FlutterGlobalOptions.kVersionFlag,
negatable: false,
help: 'Reports the version of this tool.');
argParser.addFlag('machine',
argParser.addFlag(FlutterGlobalOptions.kMachineFlag,
negatable: false,
hide: !verboseHelp,
help: 'When used with the "--version" flag, outputs the information using JSON.');
argParser.addFlag('color',
argParser.addFlag(FlutterGlobalOptions.kColorFlag,
hide: !verboseHelp,
help: 'Whether to use terminal colors (requires support for ANSI escape sequences).',
defaultsTo: true);
argParser.addFlag('version-check',
argParser.addFlag(FlutterGlobalOptions.kVersionCheckFlag,
defaultsTo: true,
hide: !verboseHelp,
help: 'Allow Flutter to check for updates when this command runs.');
argParser.addFlag('suppress-analytics',
argParser.addFlag(FlutterGlobalOptions.kSuppressAnalyticsFlag,
negatable: false,
help: 'Suppress analytics reporting for the current CLI invocation.');
argParser.addFlag('disable-telemetry',
argParser.addFlag(FlutterGlobalOptions.kDisableTelemetryFlag,
negatable: false,
help: 'Disable telemetry reporting each time a flutter or dart '
'command runs, until it is re-enabled.');
argParser.addFlag('enable-telemetry',
argParser.addFlag(FlutterGlobalOptions.kEnableTelemetryFlag,
negatable: false,
help: 'Enable telemetry reporting each time a flutter or dart '
'command runs.');
argParser.addOption('packages',
argParser.addOption(FlutterGlobalOptions.kPackagesOption,
hide: !verboseHelp,
help: 'Path to your "package_config.json" file.');
if (verboseHelp) {
argParser.addSeparator('Local build selection options (not normally required):');
}
argParser.addOption('local-engine-src-path',
argParser.addOption(FlutterGlobalOptions.kLocalEngineSrcPathOption,
hide: !verboseHelp,
help: 'Path to your engine src directory, if you are building Flutter locally.\n'
'Defaults to \$$kFlutterEngineEnvironmentVariableName if set, otherwise defaults to '
'the path given in your pubspec.yaml dependency_overrides for $kFlutterEnginePackageName, '
'if any.');
argParser.addOption('local-engine',
argParser.addOption(FlutterGlobalOptions.kLocalEngineOption,
hide: !verboseHelp,
help: 'Name of a build output within the engine out directory, if you are building Flutter locally.\n'
'Use this to select a specific version of the engine if you have built multiple engine targets.\n'
'This path is relative to "--local-engine-src-path" (see above).');
argParser.addOption('local-web-sdk',
argParser.addOption(FlutterGlobalOptions.kLocalWebSDKOption,
hide: !verboseHelp,
help: 'Name of a build output within the engine out directory, if you are building Flutter locally.\n'
'Use this to select a specific version of the web sdk if you have built multiple engine targets.\n'
......@@ -115,16 +139,22 @@ class FlutterCommandRunner extends CommandRunner<void> {
if (verboseHelp) {
argParser.addSeparator('Options for testing the "flutter" tool itself:');
}
argParser.addFlag('show-test-device',
argParser.addFlag(FlutterGlobalOptions.kShowTestDeviceFlag,
negatable: false,
hide: !verboseHelp,
help: 'List the special "flutter-tester" device in device listings. '
'This headless device is used to test Flutter tooling.');
argParser.addFlag('show-web-server-device',
argParser.addFlag(FlutterGlobalOptions.kShowWebServerDeviceFlag,
negatable: false,
hide: !verboseHelp,
help: 'List the special "web-server" device in device listings.',
);
argParser.addFlag(
FlutterGlobalOptions.kContinuousIntegrationFlag,
negatable: false,
help: 'Enable a set of CI-specific test debug settings.',
hide: !verboseHelp,
);
}
@override
......@@ -198,8 +228,8 @@ class FlutterCommandRunner extends CommandRunner<void> {
// If the flag for enabling or disabling telemetry is passed in,
// we will return out
if (topLevelResults.wasParsed('disable-telemetry') ||
topLevelResults.wasParsed('enable-telemetry')) {
if (topLevelResults.wasParsed(FlutterGlobalOptions.kDisableTelemetryFlag) ||
topLevelResults.wasParsed(FlutterGlobalOptions.kEnableTelemetryFlag)) {
return;
}
......@@ -207,43 +237,43 @@ class FlutterCommandRunner extends CommandRunner<void> {
// wrapping will occur at this width explicitly, and won't adapt if the
// terminal size changes during a run.
int? wrapColumn;
if (topLevelResults.wasParsed('wrap-column')) {
if (topLevelResults.wasParsed(FlutterGlobalOptions.kWrapColumnOption)) {
try {
wrapColumn = int.parse(topLevelResults['wrap-column'] as String);
wrapColumn = int.parse(topLevelResults[FlutterGlobalOptions.kWrapColumnOption] as String);
if (wrapColumn < 0) {
throwToolExit(userMessages.runnerWrapColumnInvalid(topLevelResults['wrap-column']));
throwToolExit(userMessages.runnerWrapColumnInvalid(topLevelResults[FlutterGlobalOptions.kWrapColumnOption]));
}
} on FormatException {
throwToolExit(userMessages.runnerWrapColumnParseError(topLevelResults['wrap-column']));
throwToolExit(userMessages.runnerWrapColumnParseError(topLevelResults[FlutterGlobalOptions.kWrapColumnOption]));
}
}
// If we're not writing to a terminal with a defined width, then don't wrap
// anything, unless the user explicitly said to.
final bool useWrapping = topLevelResults.wasParsed('wrap')
? topLevelResults['wrap'] as bool
: globals.stdio.terminalColumns != null && topLevelResults['wrap'] as bool;
final bool useWrapping = topLevelResults.wasParsed(FlutterGlobalOptions.kWrapFlag)
? topLevelResults[FlutterGlobalOptions.kWrapFlag] as bool
: globals.stdio.terminalColumns != null && topLevelResults[FlutterGlobalOptions.kWrapFlag] as bool;
contextOverrides[OutputPreferences] = OutputPreferences(
wrapText: useWrapping,
showColor: topLevelResults['color'] as bool?,
showColor: topLevelResults[FlutterGlobalOptions.kColorFlag] as bool?,
wrapColumn: wrapColumn,
);
if (((topLevelResults['show-test-device'] as bool?) ?? false)
|| topLevelResults['device-id'] == FlutterTesterDevices.kTesterDeviceId) {
if (((topLevelResults[FlutterGlobalOptions.kShowTestDeviceFlag] as bool?) ?? false)
|| topLevelResults[FlutterGlobalOptions.kDeviceIdOption] == FlutterTesterDevices.kTesterDeviceId) {
FlutterTesterDevices.showFlutterTesterDevice = true;
}
if (((topLevelResults['show-web-server-device'] as bool?) ?? false)
|| topLevelResults['device-id'] == WebServerDevice.kWebServerDeviceId) {
if (((topLevelResults[FlutterGlobalOptions.kShowWebServerDeviceFlag] as bool?) ?? false)
|| topLevelResults[FlutterGlobalOptions.kDeviceIdOption] == WebServerDevice.kWebServerDeviceId) {
WebServerDevice.showWebServerDevice = true;
}
// Set up the tooling configuration.
final EngineBuildPaths? engineBuildPaths = await globals.localEngineLocator?.findEnginePath(
engineSourcePath: topLevelResults['local-engine-src-path'] as String?,
localEngine: topLevelResults['local-engine'] as String?,
localWebSdk: topLevelResults['local-web-sdk'] as String?,
packagePath: topLevelResults['packages'] as String?,
engineSourcePath: topLevelResults[FlutterGlobalOptions.kLocalEngineSrcPathOption] as String?,
localEngine: topLevelResults[FlutterGlobalOptions.kLocalEngineOption] as String?,
localWebSdk: topLevelResults[FlutterGlobalOptions.kLocalWebSDKOption] as String?,
packagePath: topLevelResults[FlutterGlobalOptions.kPackagesOption] as String?,
);
if (engineBuildPaths != null) {
contextOverrides.addAll(<Type, Object?>{
......@@ -256,24 +286,24 @@ class FlutterCommandRunner extends CommandRunner<void> {
return MapEntry<Type, Generator>(type, () => value);
}),
body: () async {
globals.logger.quiet = (topLevelResults['quiet'] as bool?) ?? false;
globals.logger.quiet = (topLevelResults[FlutterGlobalOptions.kQuietFlag] as bool?) ?? false;
if (globals.platform.environment['FLUTTER_ALREADY_LOCKED'] != 'true') {
await globals.cache.lock();
}
if ((topLevelResults['suppress-analytics'] as bool?) ?? false) {
if ((topLevelResults[FlutterGlobalOptions.kSuppressAnalyticsFlag] as bool?) ?? false) {
globals.flutterUsage.suppressAnalytics = true;
}
globals.flutterVersion.ensureVersionFile();
final bool machineFlag = topLevelResults['machine'] as bool? ?? false;
final bool machineFlag = topLevelResults[FlutterGlobalOptions.kMachineFlag] as bool? ?? false;
final bool ci = await globals.botDetector.isRunningOnBot;
final bool redirectedCompletion = !globals.stdio.hasTerminal &&
(topLevelResults.command?.name ?? '').endsWith('-completion');
final bool isMachine = machineFlag || ci || redirectedCompletion;
final bool versionCheckFlag = topLevelResults['version-check'] as bool? ?? false;
final bool explicitVersionCheckPassed = topLevelResults.wasParsed('version-check') && versionCheckFlag;
final bool versionCheckFlag = topLevelResults[FlutterGlobalOptions.kVersionCheckFlag] as bool? ?? false;
final bool explicitVersionCheckPassed = topLevelResults.wasParsed(FlutterGlobalOptions.kVersionCheckFlag) && versionCheckFlag;
if (topLevelResults.command?.name != 'upgrade' &&
(explicitVersionCheckPassed || (versionCheckFlag && !isMachine))) {
......@@ -281,13 +311,13 @@ class FlutterCommandRunner extends CommandRunner<void> {
}
// See if the user specified a specific device.
final String? specifiedDeviceId = topLevelResults['device-id'] as String?;
final String? specifiedDeviceId = topLevelResults[FlutterGlobalOptions.kDeviceIdOption] as String?;
if (specifiedDeviceId != null) {
globals.deviceManager?.specifiedDeviceId = specifiedDeviceId;
}
if ((topLevelResults['version'] as bool?) ?? false) {
globals.flutterUsage.sendCommand('version');
if ((topLevelResults[FlutterGlobalOptions.kVersionFlag] as bool?) ?? false) {
globals.flutterUsage.sendCommand(FlutterGlobalOptions.kVersionFlag);
globals.flutterVersion.fetchTagsAndUpdate();
String status;
if (machineFlag) {
......
......@@ -1381,6 +1381,7 @@ class FakeIOSDevice extends Fake implements IOSDevice {
DeviceLogReader getLogReader({
IOSApp? app,
bool includePastLogs = false,
bool usingCISystem = false,
}) {
if (onGetLogReader == null) {
throw UnimplementedError(
......
......@@ -425,6 +425,7 @@ void main() {
'--enable-software-rendering',
'--skia-deterministic-rendering',
'--enable-embedder-api',
'--ci',
]), throwsToolExit());
final DebuggingOptions options = await command.createDebuggingOptions(false);
......@@ -440,6 +441,7 @@ void main() {
expect(options.traceSystrace, true);
expect(options.enableSoftwareRendering, true);
expect(options.skiaDeterministicRendering, true);
expect(options.usingCISystem, true);
}, overrides: <Type, Generator>{
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
......
......@@ -1096,6 +1096,7 @@ void main() {
'--enable-software-rendering',
'--skia-deterministic-rendering',
'--enable-embedder-api',
'--ci',
]), throwsToolExit());
final DebuggingOptions options = await command.createDebuggingOptions(false);
......@@ -1114,6 +1115,7 @@ void main() {
expect(options.impellerForceGL, true);
expect(options.enableSoftwareRendering, true);
expect(options.skiaDeterministicRendering, true);
expect(options.usingCISystem, true);
}, overrides: <Type, Generator>{
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
......
......@@ -30,11 +30,53 @@ void main() {
}
}));
testUsingContext('Global arg results are available in FlutterCommands', () async {
final DummyFlutterCommand command = DummyFlutterCommand(
commandFunction: () async {
return const FlutterCommandResult(ExitStatus.success);
},
);
final FlutterCommandRunner runner = FlutterCommandRunner(verboseHelp: true);
runner.addCommand(command);
await runner.run(<String>['dummy', '--${FlutterGlobalOptions.kContinuousIntegrationFlag}']);
expect(command.globalResults, isNotNull);
expect(command.boolArg(FlutterGlobalOptions.kContinuousIntegrationFlag, global: true), true);
});
testUsingContext('Global arg results are available in FlutterCommands sub commands', () async {
final DummyFlutterCommand command = DummyFlutterCommand(
commandFunction: () async {
return const FlutterCommandResult(ExitStatus.success);
},
);
final DummyFlutterCommand subcommand = DummyFlutterCommand(
name: 'sub',
commandFunction: () async {
return const FlutterCommandResult(ExitStatus.success);
},
);
command.addSubcommand(subcommand);
final FlutterCommandRunner runner = FlutterCommandRunner(verboseHelp: true);
runner.addCommand(command);
runner.addCommand(subcommand);
await runner.run(<String>['dummy', 'sub', '--${FlutterGlobalOptions.kContinuousIntegrationFlag}']);
expect(subcommand.globalResults, isNotNull);
expect(subcommand.boolArg(FlutterGlobalOptions.kContinuousIntegrationFlag, global: true), true);
});
testUsingContext('bool? safe argResults', () async {
final DummyFlutterCommand command = DummyFlutterCommand(
commandFunction: () async {
return const FlutterCommandResult(ExitStatus.success);
}
},
);
final FlutterCommandRunner runner = FlutterCommandRunner(verboseHelp: true);
command.argParser.addFlag('key');
......@@ -60,7 +102,7 @@ void main() {
final DummyFlutterCommand command = DummyFlutterCommand(
commandFunction: () async {
return const FlutterCommandResult(ExitStatus.success);
}
},
);
final FlutterCommandRunner runner = FlutterCommandRunner(verboseHelp: true);
command.argParser.addOption('key');
......@@ -82,7 +124,7 @@ void main() {
final DummyFlutterCommand command = DummyFlutterCommand(
commandFunction: () async {
return const FlutterCommandResult(ExitStatus.success);
}
},
);
final FlutterCommandRunner runner = FlutterCommandRunner(verboseHelp: true);
command.argParser.addMultiOption(
......
......@@ -348,6 +348,165 @@ Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt
);
});
});
group('both syslog and debugger stream', () {
testWithoutContext('useBothLogDeviceReaders is true when CI option is true and sdk is at least 16', () {
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
iMobileDevice: IMobileDevice(
artifacts: artifacts,
processManager: processManager,
cache: fakeCache,
logger: logger,
),
usingCISystem: true,
majorSdkVersion: 16,
);
expect(logReader.useBothLogDeviceReaders, isTrue);
});
testWithoutContext('useBothLogDeviceReaders is false when sdk is less than 16', () {
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
iMobileDevice: IMobileDevice(
artifacts: artifacts,
processManager: processManager,
cache: fakeCache,
logger: logger,
),
usingCISystem: true,
majorSdkVersion: 15,
);
expect(logReader.useBothLogDeviceReaders, isFalse);
});
testWithoutContext('useBothLogDeviceReaders is false when CI option is false', () {
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
iMobileDevice: IMobileDevice(
artifacts: artifacts,
processManager: processManager,
cache: fakeCache,
logger: logger,
),
majorSdkVersion: 16,
);
expect(logReader.useBothLogDeviceReaders, isFalse);
});
testWithoutContext('syslog only sends flutter messages to stream when useBothLogDeviceReaders is true', () async {
processManager.addCommand(
FakeCommand(
command: <String>[
ideviceSyslogPath, '-u', '1234',
],
stdout: '''
Runner(Flutter)[297] <Notice>: A is for ari
Runner(Flutter)[297] <Notice>: I is for ichigo
May 30 13:56:28 Runner(Flutter)[2037] <Notice>: flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/
May 30 13:56:28 Runner(Flutter)[2037] <Notice>: flutter: This is a test
May 30 13:56:28 Runner(Flutter)[2037] <Notice>: [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<String> lines = await logReader.logLines.toList();
expect(logReader.useBothLogDeviceReaders, isTrue);
expect(processManager, hasNoRemainingExpectations);
expect(lines, <String>[
'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/',
'flutter: This is a test'
]);
});
testWithoutContext('IOSDeviceLogReader uses both syslog and ios-deploy debugger', () async {
processManager.addCommand(
FakeCommand(
command: <String>[
ideviceSyslogPath, '-u', '1234',
],
stdout: '''
May 30 13:56:28 Runner(Flutter)[2037] <Notice>: flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/
May 30 13:56:28 Runner(Flutter)[2037] <Notice>: flutter: Check for duplicate
May 30 13:56:28 Runner(Flutter)[2037] <Notice>: [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend.
'''
),
);
final Stream<String> debuggingLogs = Stream<String>.fromIterable(<String>[
'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.',
]);
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<List<String>> logLines = logReader.logLines.toList();
final List<String> lines = await logLines;
expect(logReader.useBothLogDeviceReaders, isTrue);
expect(processManager, hasNoRemainingExpectations);
expect(lines.length, 3);
expect(lines, containsAll(<String>[
'(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('IOSDeviceLogReader only uses ios-deploy debugger when useBothLogDeviceReaders is false', () async {
final Stream<String> debuggingLogs = Stream<String>.fromIterable(<String>[
'(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.logLines = debuggingLogs;
logReader.debuggerStream = iosDeployDebugger;
final Future<List<String>> logLines = logReader.logLines.toList();
final List<String> lines = await logLines;
expect(logReader.useBothLogDeviceReaders, 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,
);
});
});
}
class FakeIOSDeployDebugger extends Fake implements IOSDeployDebugger {
......
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