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) { ...@@ -471,6 +471,10 @@ List<String> _flutterCommandArgs(String command, List<String> options) {
if (localEngineSrcPath != null) ...<String>['--local-engine-src-path', localEngineSrcPath], if (localEngineSrcPath != null) ...<String>['--local-engine-src-path', localEngineSrcPath],
if (localWebSdk != null) ...<String>['--local-web-sdk', localWebSdk], if (localWebSdk != null) ...<String>['--local-web-sdk', localWebSdk],
...options, ...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'; ...@@ -33,6 +33,7 @@ import '../resident_runner.dart';
import '../run_cold.dart'; import '../run_cold.dart';
import '../run_hot.dart'; import '../run_hot.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
import '../runner/flutter_command_runner.dart';
import '../vmservice.dart'; import '../vmservice.dart';
/// A Flutter-command that attaches to applications that have been launched /// 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. ...@@ -528,6 +529,7 @@ known, it can be explicitly provided to attach via the command-line, e.g.
ddsPort: ddsPort, ddsPort: ddsPort,
devToolsServerAddress: devToolsServerAddress, devToolsServerAddress: devToolsServerAddress,
serveObservatory: serveObservatory, serveObservatory: serveObservatory,
usingCISystem: usingCISystem,
); );
return buildInfo.isDebug return buildInfo.isDebug
...@@ -535,7 +537,7 @@ known, it can be explicitly provided to attach via the command-line, e.g. ...@@ -535,7 +537,7 @@ known, it can be explicitly provided to attach via the command-line, e.g.
flutterDevices, flutterDevices,
target: targetFile, target: targetFile,
debuggingOptions: debuggingOptions, debuggingOptions: debuggingOptions,
packagesFilePath: globalResults!['packages'] as String?, packagesFilePath: globalResults![FlutterGlobalOptions.kPackagesOption] as String?,
projectRootPath: stringArg('project-root'), projectRootPath: stringArg('project-root'),
dillOutputPath: stringArg('output-dill'), dillOutputPath: stringArg('output-dill'),
ipv6: usesIpv6, ipv6: usesIpv6,
......
...@@ -6,6 +6,7 @@ import '../base/common.dart'; ...@@ -6,6 +6,7 @@ import '../base/common.dart';
import '../cache.dart'; import '../cache.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
import '../runner/flutter_command_runner.dart';
import '../version.dart'; import '../version.dart';
class ChannelCommand extends FlutterCommand { class ChannelCommand extends FlutterCommand {
...@@ -40,7 +41,7 @@ class ChannelCommand extends FlutterCommand { ...@@ -40,7 +41,7 @@ class ChannelCommand extends FlutterCommand {
case 0: case 0:
await _listChannels( await _listChannels(
showAll: boolArg('all'), showAll: boolArg('all'),
verbose: globalResults?['verbose'] == true, verbose: globalResults?[FlutterGlobalOptions.kVerboseFlag] == true,
); );
return FlutterCommandResult.success(); return FlutterCommandResult.success();
case 1: case 1:
......
...@@ -25,6 +25,7 @@ import '../custom_devices/custom_devices_config.dart'; ...@@ -25,6 +25,7 @@ import '../custom_devices/custom_devices_config.dart';
import '../device_port_forwarder.dart'; import '../device_port_forwarder.dart';
import '../features.dart'; import '../features.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
import '../runner/flutter_command_runner.dart';
/// just the function signature of the [print] function. /// just the function signature of the [print] function.
/// The Object arg may be null. /// The Object arg may be null.
...@@ -811,7 +812,7 @@ Delete a device from the config file. ...@@ -811,7 +812,7 @@ Delete a device from the config file.
Future<FlutterCommandResult> runCommand() async { Future<FlutterCommandResult> runCommand() async {
checkFeatureEnabled(); checkFeatureEnabled();
final String? id = globalResults!['device-id'] as String?; final String? id = globalResults![FlutterGlobalOptions.kDeviceIdOption] as String?;
if (id == null || !customDevicesConfig.contains(id)) { if (id == null || !customDevicesConfig.contains(id)) {
throwToolExit('Couldn\'t find device with id "$id" in config at "${customDevicesConfig.configPath}"'); throwToolExit('Couldn\'t find device with id "$id" in config at "${customDevicesConfig.configPath}"');
} }
......
...@@ -26,6 +26,7 @@ import '../resident_runner.dart'; ...@@ -26,6 +26,7 @@ import '../resident_runner.dart';
import '../run_cold.dart'; import '../run_cold.dart';
import '../run_hot.dart'; import '../run_hot.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
import '../runner/flutter_command_runner.dart';
import '../tracing.dart'; import '../tracing.dart';
import '../vmservice.dart'; import '../vmservice.dart';
import '../web/web_runner.dart'; import '../web/web_runner.dart';
...@@ -247,6 +248,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment ...@@ -247,6 +248,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment
uninstallFirst: uninstallFirst, uninstallFirst: uninstallFirst,
enableDartProfiling: enableDartProfiling, enableDartProfiling: enableDartProfiling,
enableEmbedderApi: enableEmbedderApi, enableEmbedderApi: enableEmbedderApi,
usingCISystem: usingCISystem,
); );
} else { } else {
return DebuggingOptions.enabled( return DebuggingOptions.enabled(
...@@ -298,6 +300,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment ...@@ -298,6 +300,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment
serveObservatory: boolArg('serve-observatory'), serveObservatory: boolArg('serve-observatory'),
enableDartProfiling: enableDartProfiling, enableDartProfiling: enableDartProfiling,
enableEmbedderApi: enableEmbedderApi, enableEmbedderApi: enableEmbedderApi,
usingCISystem: usingCISystem,
); );
} }
} }
...@@ -643,7 +646,7 @@ class RunCommand extends RunCommandBase { ...@@ -643,7 +646,7 @@ class RunCommand extends RunCommandBase {
: globals.fs.file(applicationBinaryPath), : globals.fs.file(applicationBinaryPath),
trackWidgetCreation: trackWidgetCreation, trackWidgetCreation: trackWidgetCreation,
projectRootPath: stringArg('project-root'), projectRootPath: stringArg('project-root'),
packagesFilePath: globalResults!['packages'] as String?, packagesFilePath: globalResults![FlutterGlobalOptions.kPackagesOption] as String?,
dillOutputPath: stringArg('output-dill'), dillOutputPath: stringArg('output-dill'),
ipv6: ipv6 ?? false, ipv6: ipv6 ?? false,
multidexEnabled: boolArg('multidex'), multidexEnabled: boolArg('multidex'),
......
...@@ -422,6 +422,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { ...@@ -422,6 +422,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
disablePortPublication: true, disablePortPublication: true,
enableDds: enableDds, enableDds: enableDds,
nullAssertions: boolArg(FlutterOptions.kNullAssertions), nullAssertions: boolArg(FlutterOptions.kNullAssertions),
usingCISystem: usingCISystem,
); );
Device? integrationTestDevice; Device? integrationTestDevice;
......
...@@ -971,6 +971,7 @@ class DebuggingOptions { ...@@ -971,6 +971,7 @@ class DebuggingOptions {
this.serveObservatory = false, this.serveObservatory = false,
this.enableDartProfiling = true, this.enableDartProfiling = true,
this.enableEmbedderApi = false, this.enableEmbedderApi = false,
this.usingCISystem = false,
}) : debuggingEnabled = true; }) : debuggingEnabled = true;
DebuggingOptions.disabled(this.buildInfo, { DebuggingOptions.disabled(this.buildInfo, {
...@@ -993,6 +994,7 @@ class DebuggingOptions { ...@@ -993,6 +994,7 @@ class DebuggingOptions {
this.uninstallFirst = false, this.uninstallFirst = false,
this.enableDartProfiling = true, this.enableDartProfiling = true,
this.enableEmbedderApi = false, this.enableEmbedderApi = false,
this.usingCISystem = false,
}) : debuggingEnabled = false, }) : debuggingEnabled = false,
useTestFonts = false, useTestFonts = false,
startPaused = false, startPaused = false,
...@@ -1069,6 +1071,7 @@ class DebuggingOptions { ...@@ -1069,6 +1071,7 @@ class DebuggingOptions {
required this.serveObservatory, required this.serveObservatory,
required this.enableDartProfiling, required this.enableDartProfiling,
required this.enableEmbedderApi, required this.enableEmbedderApi,
required this.usingCISystem,
}); });
final bool debuggingEnabled; final bool debuggingEnabled;
...@@ -1109,6 +1112,7 @@ class DebuggingOptions { ...@@ -1109,6 +1112,7 @@ class DebuggingOptions {
final bool serveObservatory; final bool serveObservatory;
final bool enableDartProfiling; final bool enableDartProfiling;
final bool enableEmbedderApi; final bool enableEmbedderApi;
final bool usingCISystem;
/// Whether the tool should try to uninstall a previously installed version of the app. /// Whether the tool should try to uninstall a previously installed version of the app.
/// ///
...@@ -1243,6 +1247,7 @@ class DebuggingOptions { ...@@ -1243,6 +1247,7 @@ class DebuggingOptions {
'serveObservatory': serveObservatory, 'serveObservatory': serveObservatory,
'enableDartProfiling': enableDartProfiling, 'enableDartProfiling': enableDartProfiling,
'enableEmbedderApi': enableEmbedderApi, 'enableEmbedderApi': enableEmbedderApi,
'usingCISystem': usingCISystem,
}; };
static DebuggingOptions fromJson(Map<String, Object?> json, BuildInfo buildInfo) => static DebuggingOptions fromJson(Map<String, Object?> json, BuildInfo buildInfo) =>
...@@ -1294,6 +1299,7 @@ class DebuggingOptions { ...@@ -1294,6 +1299,7 @@ class DebuggingOptions {
serveObservatory: (json['serveObservatory'] as bool?) ?? false, serveObservatory: (json['serveObservatory'] as bool?) ?? false,
enableDartProfiling: (json['enableDartProfiling'] as bool?) ?? true, enableDartProfiling: (json['enableDartProfiling'] as bool?) ?? true,
enableEmbedderApi: (json['enableEmbedderApi'] as bool?) ?? false, enableEmbedderApi: (json['enableEmbedderApi'] as bool?) ?? false,
usingCISystem: (json['usingCISystem'] as bool?) ?? false,
); );
} }
......
...@@ -482,7 +482,10 @@ class IOSDevice extends Device { ...@@ -482,7 +482,10 @@ class IOSDevice extends Device {
int installationResult = 1; int installationResult = 1;
if (debuggingOptions.debuggingEnabled) { if (debuggingOptions.debuggingEnabled) {
_logger.printTrace('Debugging is enabled, connecting to vmService'); _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 // If the device supports syslog reading, prefer launching the app without
// attaching the debugger to avoid the overhead of the unnecessary extra running process. // attaching the debugger to avoid the overhead of the unnecessary extra running process.
...@@ -629,12 +632,14 @@ class IOSDevice extends Device { ...@@ -629,12 +632,14 @@ class IOSDevice extends Device {
DeviceLogReader getLogReader({ DeviceLogReader getLogReader({
covariant IOSApp? app, covariant IOSApp? app,
bool includePastLogs = false, bool includePastLogs = false,
bool usingCISystem = false,
}) { }) {
assert(!includePastLogs, 'Past log reading not supported on iOS devices.'); assert(!includePastLogs, 'Past log reading not supported on iOS devices.');
return _logReaders.putIfAbsent(app, () => IOSDeviceLogReader.create( return _logReaders.putIfAbsent(app, () => IOSDeviceLogReader.create(
device: this, device: this,
app: app, app: app,
iMobileDevice: _iMobileDevice, iMobileDevice: _iMobileDevice,
usingCISystem: usingCISystem,
)); ));
} }
...@@ -749,17 +754,20 @@ class IOSDeviceLogReader extends DeviceLogReader { ...@@ -749,17 +754,20 @@ class IOSDeviceLogReader extends DeviceLogReader {
this._deviceId, this._deviceId,
this.name, this.name,
String appName, String appName,
bool usingCISystem,
) : // Match for lines for the runner in syslog. ) : // Match for lines for the runner in syslog.
// //
// iOS 9 format: Runner[297] <Notice>: // iOS 9 format: Runner[297] <Notice>:
// iOS 10 format: Runner(Flutter)[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]. /// Create a new [IOSDeviceLogReader].
factory IOSDeviceLogReader.create({ factory IOSDeviceLogReader.create({
required IOSDevice device, required IOSDevice device,
IOSApp? app, IOSApp? app,
required IMobileDevice iMobileDevice, required IMobileDevice iMobileDevice,
bool usingCISystem = false,
}) { }) {
final String appName = app?.name?.replaceAll('.app', '') ?? ''; final String appName = app?.name?.replaceAll('.app', '') ?? '';
return IOSDeviceLogReader._( return IOSDeviceLogReader._(
...@@ -768,6 +776,7 @@ class IOSDeviceLogReader extends DeviceLogReader { ...@@ -768,6 +776,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
device.id, device.id,
device.name, device.name,
appName, appName,
usingCISystem,
); );
} }
...@@ -775,9 +784,17 @@ class IOSDeviceLogReader extends DeviceLogReader { ...@@ -775,9 +784,17 @@ class IOSDeviceLogReader extends DeviceLogReader {
factory IOSDeviceLogReader.test({ factory IOSDeviceLogReader.test({
required IMobileDevice iMobileDevice, required IMobileDevice iMobileDevice,
bool useSyslog = true, bool useSyslog = true,
bool usingCISystem = false,
int? majorSdkVersion,
}) { }) {
final int sdkVersion;
if (majorSdkVersion != null) {
sdkVersion = majorSdkVersion;
} else {
sdkVersion = useSyslog ? 12 : 13;
}
return IOSDeviceLogReader._( return IOSDeviceLogReader._(
iMobileDevice, useSyslog ? 12 : 13, '1234', 'test', 'Runner'); iMobileDevice, sdkVersion, '1234', 'test', 'Runner', usingCISystem);
} }
@override @override
...@@ -785,6 +802,7 @@ class IOSDeviceLogReader extends DeviceLogReader { ...@@ -785,6 +802,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
final int _majorSdkVersion; final int _majorSdkVersion;
final String _deviceId; final String _deviceId;
final IMobileDevice _iMobileDevice; final IMobileDevice _iMobileDevice;
final bool _usingCISystem;
// Matches a syslog line from the runner. // Matches a syslog line from the runner.
RegExp _runnerLineRegex; RegExp _runnerLineRegex;
...@@ -810,12 +828,42 @@ class IOSDeviceLogReader extends DeviceLogReader { ...@@ -810,12 +828,42 @@ class IOSDeviceLogReader extends DeviceLogReader {
// Sometimes (race condition?) we try to send a log after the controller has // 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 // been closed. See https://github.com/flutter/flutter/issues/99021 for more
// context. // context.
void _addToLinesController(String message) { void _addToLinesController(String message, IOSDeviceLogSource source) {
if (!linesController.isClosed) { if (!linesController.isClosed) {
if (_excludeLog(message, source)) {
return;
}
linesController.add(message); 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>>[]; final List<StreamSubscription<void>> _loggingSubscriptions = <StreamSubscription<void>>[];
@override @override
...@@ -835,6 +883,10 @@ class IOSDeviceLogReader extends DeviceLogReader { ...@@ -835,6 +883,10 @@ class IOSDeviceLogReader extends DeviceLogReader {
static const int minimumUniversalLoggingSdkVersion = 13; 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 { Future<void> _listenToUnifiedLoggingEvents(FlutterVmService connectedVmService) async {
if (_majorSdkVersion < minimumUniversalLoggingSdkVersion) { if (_majorSdkVersion < minimumUniversalLoggingSdkVersion) {
return; return;
...@@ -859,7 +911,7 @@ class IOSDeviceLogReader extends DeviceLogReader { ...@@ -859,7 +911,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
} }
final String message = processVmServiceMessage(event); final String message = processVmServiceMessage(event);
if (message.isNotEmpty) { if (message.isNotEmpty) {
_addToLinesController(message); _addToLinesController(message, IOSDeviceLogSource.unifiedLogging);
} }
} }
...@@ -871,8 +923,10 @@ class IOSDeviceLogReader extends DeviceLogReader { ...@@ -871,8 +923,10 @@ class IOSDeviceLogReader extends DeviceLogReader {
/// Log reader will listen to [debugger.logLines] and will detach debugger on dispose. /// Log reader will listen to [debugger.logLines] and will detach debugger on dispose.
IOSDeployDebugger? get debuggerStream => _iosDeployDebugger; IOSDeployDebugger? get debuggerStream => _iosDeployDebugger;
/// Send messages from ios-deploy debugger stream to device log reader stream.
set debuggerStream(IOSDeployDebugger? debugger) { 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) { if (_majorSdkVersion < minimumUniversalLoggingSdkVersion) {
return; return;
} }
...@@ -882,7 +936,10 @@ class IOSDeviceLogReader extends DeviceLogReader { ...@@ -882,7 +936,10 @@ class IOSDeviceLogReader extends DeviceLogReader {
} }
// Add the debugger logs to the controller created on initialization. // Add the debugger logs to the controller created on initialization.
_loggingSubscriptions.add(debugger.logLines.listen( _loggingSubscriptions.add(debugger.logLines.listen(
(String line) => _addToLinesController(_debuggerLineHandler(line)), (String line) => _addToLinesController(
_debuggerLineHandler(line),
IOSDeviceLogSource.iosDeploy,
),
onError: linesController.addError, onError: linesController.addError,
onDone: linesController.close, onDone: linesController.close,
cancelOnError: true, cancelOnError: true,
...@@ -893,18 +950,38 @@ class IOSDeviceLogReader extends DeviceLogReader { ...@@ -893,18 +950,38 @@ class IOSDeviceLogReader extends DeviceLogReader {
// Strip off the logging metadata (leave the category), or just echo the line. // Strip off the logging metadata (leave the category), or just echo the line.
String _debuggerLineHandler(String line) => _debuggerLoggingRegex.firstMatch(line)?.group(1) ?? 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() { void _listenToSysLog() {
// syslog is not written on iOS 13+. // Syslog stopped working on iOS 13 (https://github.com/flutter/flutter/issues/41133).
if (_majorSdkVersion >= minimumUniversalLoggingSdkVersion) { // 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; return;
} }
_iMobileDevice.startLogger(_deviceId).then<void>((Process process) { _iMobileDevice.startLogger(_deviceId).then<void>((Process process) {
process.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_newSyslogLineHandler()); 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.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_newSyslogLineHandler());
process.exitCode.whenComplete(() { process.exitCode.whenComplete(() {
if (linesController.hasListener) { if (!linesController.hasListener) {
linesController.close(); 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); assert(idevicesyslogProcess == null);
idevicesyslogProcess = process; idevicesyslogProcess = process;
...@@ -926,7 +1003,7 @@ class IOSDeviceLogReader extends DeviceLogReader { ...@@ -926,7 +1003,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
return (String line) { return (String line) {
if (printing) { if (printing) {
if (!_anyLineRegex.hasMatch(line)) { if (!_anyLineRegex.hasMatch(line)) {
_addToLinesController(decodeSyslog(line)); _addToLinesController(decodeSyslog(line), IOSDeviceLogSource.idevicesyslog);
return; return;
} }
...@@ -938,8 +1015,7 @@ class IOSDeviceLogReader extends DeviceLogReader { ...@@ -938,8 +1015,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
if (match != null) { if (match != null) {
final String logLine = line.substring(match.end); final String logLine = line.substring(match.end);
// Only display the log line after the initial device and executable information. // Only display the log line after the initial device and executable information.
_addToLinesController(decodeSyslog(logLine)); _addToLinesController(decodeSyslog(logLine), IOSDeviceLogSource.idevicesyslog);
printing = true; printing = true;
} }
}; };
...@@ -955,6 +1031,15 @@ class IOSDeviceLogReader extends DeviceLogReader { ...@@ -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. /// A [DevicePortForwarder] specialized for iOS usage with iproxy.
class IOSDevicePortForwarder extends DevicePortForwarder { class IOSDevicePortForwarder extends DevicePortForwarder {
......
...@@ -36,6 +36,8 @@ import 'devfs.dart'; ...@@ -36,6 +36,8 @@ import 'devfs.dart';
import 'device.dart'; import 'device.dart';
import 'features.dart'; import 'features.dart';
import 'globals.dart' as globals; import 'globals.dart' as globals;
import 'ios/application_package.dart';
import 'ios/devices.dart';
import 'project.dart'; import 'project.dart';
import 'resident_devtools_handler.dart'; import 'resident_devtools_handler.dart';
import 'run_cold.dart'; import 'run_cold.dart';
...@@ -391,11 +393,19 @@ class FlutterDevice { ...@@ -391,11 +393,19 @@ class FlutterDevice {
return devFS!.create(); return devFS!.create();
} }
Future<void> startEchoingDeviceLog() async { Future<void> startEchoingDeviceLog(DebuggingOptions debuggingOptions) async {
if (_loggingSubscription != null) { if (_loggingSubscription != null) {
return; 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) { _loggingSubscription = logStream.listen((String line) {
if (!line.contains(globals.kVMServiceMessageRegExp)) { if (!line.contains(globals.kVMServiceMessageRegExp)) {
globals.printStatus(line, wrap: false); globals.printStatus(line, wrap: false);
...@@ -451,7 +461,7 @@ class FlutterDevice { ...@@ -451,7 +461,7 @@ class FlutterDevice {
'multidex': hotRunner.multidexEnabled, 'multidex': hotRunner.multidexEnabled,
}; };
await startEchoingDeviceLog(); await startEchoingDeviceLog(hotRunner.debuggingOptions);
// Start the application. // Start the application.
final Future<LaunchResult> futureResult = device!.startApp( final Future<LaunchResult> futureResult = device!.startApp(
...@@ -519,7 +529,7 @@ class FlutterDevice { ...@@ -519,7 +529,7 @@ class FlutterDevice {
platformArgs['trace-startup'] = coldRunner.traceStartup; platformArgs['trace-startup'] = coldRunner.traceStartup;
platformArgs['multidex'] = coldRunner.multidexEnabled; platformArgs['multidex'] = coldRunner.multidexEnabled;
await startEchoingDeviceLog(); await startEchoingDeviceLog(coldRunner.debuggingOptions);
final LaunchResult result = await device!.startApp( final LaunchResult result = await device!.startApp(
applicationPackage, applicationPackage,
......
...@@ -304,7 +304,10 @@ abstract class FlutterCommand extends Command<void> { ...@@ -304,7 +304,10 @@ abstract class FlutterCommand extends Command<void> {
/// Path to the Dart's package config file. /// Path to the Dart's package config file.
/// ///
/// This can be overridden by some of its subclasses. /// 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. /// The value of the `--filesystem-scheme` argument.
/// ///
...@@ -1634,17 +1637,30 @@ Run 'flutter -h' (or 'flutter <command> -h') for available flutter commands and ...@@ -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] /// If no flag named [name] was added to the [ArgParser], an [ArgumentError]
/// will be thrown. /// 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`. /// Gets the parsed command-line option named [name] as a `String`.
/// ///
/// If no option named [name] was added to the [ArgParser], an [ArgumentError] /// If no option named [name] was added to the [ArgParser], an [ArgumentError]
/// will be thrown. /// 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>`. /// Gets the parsed command-line option named [name] as `List<String>`.
List<String> stringsArg(String name) { List<String> stringsArg(String name, {bool global = false}) {
return argResults![name]! as List<String>? ?? <String>[]; if (global) {
return globalResults![name] as List<String>;
}
return argResults![name] as List<String>;
} }
} }
......
...@@ -1381,6 +1381,7 @@ class FakeIOSDevice extends Fake implements IOSDevice { ...@@ -1381,6 +1381,7 @@ class FakeIOSDevice extends Fake implements IOSDevice {
DeviceLogReader getLogReader({ DeviceLogReader getLogReader({
IOSApp? app, IOSApp? app,
bool includePastLogs = false, bool includePastLogs = false,
bool usingCISystem = false,
}) { }) {
if (onGetLogReader == null) { if (onGetLogReader == null) {
throw UnimplementedError( throw UnimplementedError(
......
...@@ -425,6 +425,7 @@ void main() { ...@@ -425,6 +425,7 @@ void main() {
'--enable-software-rendering', '--enable-software-rendering',
'--skia-deterministic-rendering', '--skia-deterministic-rendering',
'--enable-embedder-api', '--enable-embedder-api',
'--ci',
]), throwsToolExit()); ]), throwsToolExit());
final DebuggingOptions options = await command.createDebuggingOptions(false); final DebuggingOptions options = await command.createDebuggingOptions(false);
...@@ -440,6 +441,7 @@ void main() { ...@@ -440,6 +441,7 @@ void main() {
expect(options.traceSystrace, true); expect(options.traceSystrace, true);
expect(options.enableSoftwareRendering, true); expect(options.enableSoftwareRendering, true);
expect(options.skiaDeterministicRendering, true); expect(options.skiaDeterministicRendering, true);
expect(options.usingCISystem, true);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Cache: () => Cache.test(processManager: FakeProcessManager.any()), Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(), FileSystem: () => MemoryFileSystem.test(),
......
...@@ -1096,6 +1096,7 @@ void main() { ...@@ -1096,6 +1096,7 @@ void main() {
'--enable-software-rendering', '--enable-software-rendering',
'--skia-deterministic-rendering', '--skia-deterministic-rendering',
'--enable-embedder-api', '--enable-embedder-api',
'--ci',
]), throwsToolExit()); ]), throwsToolExit());
final DebuggingOptions options = await command.createDebuggingOptions(false); final DebuggingOptions options = await command.createDebuggingOptions(false);
...@@ -1114,6 +1115,7 @@ void main() { ...@@ -1114,6 +1115,7 @@ void main() {
expect(options.impellerForceGL, true); expect(options.impellerForceGL, true);
expect(options.enableSoftwareRendering, true); expect(options.enableSoftwareRendering, true);
expect(options.skiaDeterministicRendering, true); expect(options.skiaDeterministicRendering, true);
expect(options.usingCISystem, true);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Cache: () => Cache.test(processManager: FakeProcessManager.any()), Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(), FileSystem: () => MemoryFileSystem.test(),
......
...@@ -30,11 +30,53 @@ void main() { ...@@ -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 { testUsingContext('bool? safe argResults', () async {
final DummyFlutterCommand command = DummyFlutterCommand( final DummyFlutterCommand command = DummyFlutterCommand(
commandFunction: () async { commandFunction: () async {
return const FlutterCommandResult(ExitStatus.success); return const FlutterCommandResult(ExitStatus.success);
} },
); );
final FlutterCommandRunner runner = FlutterCommandRunner(verboseHelp: true); final FlutterCommandRunner runner = FlutterCommandRunner(verboseHelp: true);
command.argParser.addFlag('key'); command.argParser.addFlag('key');
...@@ -58,9 +100,9 @@ void main() { ...@@ -58,9 +100,9 @@ void main() {
testUsingContext('String? safe argResults', () async { testUsingContext('String? safe argResults', () async {
final DummyFlutterCommand command = DummyFlutterCommand( final DummyFlutterCommand command = DummyFlutterCommand(
commandFunction: () async { commandFunction: () async {
return const FlutterCommandResult(ExitStatus.success); return const FlutterCommandResult(ExitStatus.success);
} },
); );
final FlutterCommandRunner runner = FlutterCommandRunner(verboseHelp: true); final FlutterCommandRunner runner = FlutterCommandRunner(verboseHelp: true);
command.argParser.addOption('key'); command.argParser.addOption('key');
...@@ -80,9 +122,9 @@ void main() { ...@@ -80,9 +122,9 @@ void main() {
testUsingContext('List<String> safe argResults', () async { testUsingContext('List<String> safe argResults', () async {
final DummyFlutterCommand command = DummyFlutterCommand( final DummyFlutterCommand command = DummyFlutterCommand(
commandFunction: () async { commandFunction: () async {
return const FlutterCommandResult(ExitStatus.success); return const FlutterCommandResult(ExitStatus.success);
} },
); );
final FlutterCommandRunner runner = FlutterCommandRunner(verboseHelp: true); final FlutterCommandRunner runner = FlutterCommandRunner(verboseHelp: true);
command.argParser.addMultiOption( command.argParser.addMultiOption(
......
...@@ -348,6 +348,165 @@ Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt ...@@ -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 { 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