mirror of
https://github.com/flutter/flutter
synced 2024-10-13 03:32:55 +00:00
parent
de85509171
commit
2be4570d3a
|
@ -212,6 +212,9 @@ class IOSDevice extends Device {
|
|||
|
||||
DevicePortForwarder _portForwarder;
|
||||
|
||||
@visibleForTesting
|
||||
IOSDeployDebugger iosDeployDebugger;
|
||||
|
||||
@override
|
||||
Future<bool> get isLocalEmulator async => false;
|
||||
|
||||
|
@ -395,23 +398,38 @@ class IOSDevice extends Device {
|
|||
timeout: timeoutConfiguration.slowOperation);
|
||||
try {
|
||||
ProtocolDiscovery observatoryDiscovery;
|
||||
int installationResult = 1;
|
||||
if (debuggingOptions.debuggingEnabled) {
|
||||
_logger.printTrace('Debugging is enabled, connecting to observatory');
|
||||
iosDeployDebugger = _iosDeploy.prepareDebuggerForLaunch(
|
||||
deviceId: id,
|
||||
bundlePath: bundle.path,
|
||||
launchArguments: launchArguments,
|
||||
interfaceType: interfaceType,
|
||||
);
|
||||
|
||||
final DeviceLogReader deviceLogReader = getLogReader(app: package);
|
||||
if (deviceLogReader is IOSDeviceLogReader) {
|
||||
deviceLogReader.debuggerStream = iosDeployDebugger;
|
||||
}
|
||||
observatoryDiscovery = ProtocolDiscovery.observatory(
|
||||
getLogReader(app: package),
|
||||
deviceLogReader,
|
||||
portForwarder: portForwarder,
|
||||
throttleDuration: fallbackPollingDelay,
|
||||
throttleTimeout: fallbackThrottleTimeout ?? const Duration(seconds: 5),
|
||||
hostPort: debuggingOptions.hostVmServicePort,
|
||||
devicePort: debuggingOptions.deviceVmServicePort,
|
||||
ipv6: ipv6,
|
||||
throttleTimeout: fallbackThrottleTimeout ?? const Duration(seconds: 1),
|
||||
);
|
||||
installationResult = await iosDeployDebugger.launchAndAttach() ? 0 : 1;
|
||||
} else {
|
||||
installationResult = await _iosDeploy.launchApp(
|
||||
deviceId: id,
|
||||
bundlePath: bundle.path,
|
||||
launchArguments: launchArguments,
|
||||
interfaceType: interfaceType,
|
||||
);
|
||||
}
|
||||
final int installationResult = await _iosDeploy.runApp(
|
||||
deviceId: id,
|
||||
bundlePath: bundle.path,
|
||||
launchArguments: launchArguments,
|
||||
interfaceType: interfaceType,
|
||||
);
|
||||
if (installationResult != 0) {
|
||||
_logger.printError('Could not run ${bundle.path} on $id.');
|
||||
_logger.printError('Try launching Xcode and selecting "Product > Run" to fix the problem:');
|
||||
|
@ -465,7 +483,11 @@ class IOSDevice extends Device {
|
|||
IOSApp app, {
|
||||
String userIdentifier,
|
||||
}) async {
|
||||
// Currently we don't have a way to stop an app running on iOS.
|
||||
// If the debugger is not attached, killing the ios-deploy process won't stop the app.
|
||||
if (iosDeployDebugger!= null && iosDeployDebugger.debuggerAttached) {
|
||||
// Avoid null.
|
||||
return iosDeployDebugger?.exit() == true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -655,6 +677,13 @@ class IOSDeviceLogReader extends DeviceLogReader {
|
|||
// Matches a syslog line from any app.
|
||||
RegExp _anyLineRegex;
|
||||
|
||||
// Logging from native code/Flutter engine is prefixed by timestamp and process metadata:
|
||||
// 2020-09-15 19:15:10.931434-0700 Runner[541:226276] Did finish launching.
|
||||
// 2020-09-15 19:15:10.931434-0700 Runner[541:226276] [Category] Did finish launching.
|
||||
//
|
||||
// Logging from the dart code has no prefixing metadata.
|
||||
final RegExp _debuggerLoggingRegex = RegExp(r'^\S* \S* \S*\[[0-9:]*] (.*)');
|
||||
|
||||
StreamController<String> _linesController;
|
||||
List<StreamSubscription<void>> _loggingSubscriptions;
|
||||
|
||||
|
@ -687,6 +716,10 @@ class IOSDeviceLogReader extends DeviceLogReader {
|
|||
}
|
||||
|
||||
void logMessage(vm_service.Event event) {
|
||||
if (_iosDeployDebugger != null && _iosDeployDebugger.debuggerAttached) {
|
||||
// Prefer the more complete logs from the attached debugger.
|
||||
return;
|
||||
}
|
||||
final String message = processVmServiceMessage(event);
|
||||
if (message.isNotEmpty) {
|
||||
_linesController.add(message);
|
||||
|
@ -699,6 +732,26 @@ class IOSDeviceLogReader extends DeviceLogReader {
|
|||
]);
|
||||
}
|
||||
|
||||
/// Log reader will listen to [debugger.logLines] and will detach debugger on dispose.
|
||||
set debuggerStream(IOSDeployDebugger debugger) {
|
||||
// Logging is gathered from syslog on iOS 13 and earlier.
|
||||
if (_majorSdkVersion < _minimumUniversalLoggingSdkVersion) {
|
||||
return;
|
||||
}
|
||||
_iosDeployDebugger = debugger;
|
||||
// Add the debugger logs to the controller created on initialization.
|
||||
_loggingSubscriptions.add(debugger.logLines.listen(
|
||||
(String line) => _linesController.add(_debuggerLineHandler(line)),
|
||||
onError: _linesController.addError,
|
||||
onDone: _linesController.close,
|
||||
cancelOnError: true,
|
||||
));
|
||||
}
|
||||
IOSDeployDebugger _iosDeployDebugger;
|
||||
|
||||
// Strip off the logging metadata (leave the category), or just echo the line.
|
||||
String _debuggerLineHandler(String line) => _debuggerLoggingRegex?.firstMatch(line)?.group(1) ?? line;
|
||||
|
||||
void _listenToSysLog() {
|
||||
// syslog is not written on iOS 13+.
|
||||
if (_majorSdkVersion >= _minimumUniversalLoggingSdkVersion) {
|
||||
|
@ -758,6 +811,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
|
|||
loggingSubscription.cancel();
|
||||
}
|
||||
_idevicesyslogProcess?.kill();
|
||||
_iosDeployDebugger?.detach();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -81,6 +81,29 @@ class FallbackDiscovery {
|
|||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
final Uri result = await _protocolDiscovery.uri;
|
||||
if (result != null) {
|
||||
UsageEvent(
|
||||
_kEventName,
|
||||
'log-success',
|
||||
flutterUsage: _flutterUsage,
|
||||
).send();
|
||||
return result;
|
||||
}
|
||||
} on ArgumentError {
|
||||
// In the event of an invalid InternetAddress, this code attempts to catch
|
||||
// an ArgumentError from protocol_discovery.dart
|
||||
} on Exception catch (err) {
|
||||
_logger.printTrace(err.toString());
|
||||
}
|
||||
_logger.printTrace('Failed to connect with log scanning, falling back to mDNS');
|
||||
UsageEvent(
|
||||
_kEventName,
|
||||
'log-failure',
|
||||
flutterUsage: _flutterUsage,
|
||||
).send();
|
||||
|
||||
try {
|
||||
final Uri result = await _mDnsObservatoryDiscovery.getObservatoryUri(
|
||||
packageId,
|
||||
|
@ -99,35 +122,12 @@ class FallbackDiscovery {
|
|||
} on Exception catch (err) {
|
||||
_logger.printTrace(err.toString());
|
||||
}
|
||||
_logger.printTrace('Failed to connect with mDNS, falling back to log scanning');
|
||||
_logger.printTrace('Failed to connect with mDNS');
|
||||
UsageEvent(
|
||||
_kEventName,
|
||||
'mdns-failure',
|
||||
flutterUsage: _flutterUsage,
|
||||
).send();
|
||||
|
||||
try {
|
||||
final Uri result = await _protocolDiscovery.uri;
|
||||
if (result != null) {
|
||||
UsageEvent(
|
||||
_kEventName,
|
||||
'fallback-success',
|
||||
flutterUsage: _flutterUsage,
|
||||
).send();
|
||||
return result;
|
||||
}
|
||||
} on ArgumentError {
|
||||
// In the event of an invalid InternetAddress, this code attempts to catch
|
||||
// an ArgumentError from protocol_discovery.dart
|
||||
} on Exception catch (err) {
|
||||
_logger.printTrace(err.toString());
|
||||
}
|
||||
_logger.printTrace('Failed to connect with log scanning');
|
||||
UsageEvent(
|
||||
_kEventName,
|
||||
'fallback-failure',
|
||||
flutterUsage: _flutterUsage,
|
||||
).send();
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -148,7 +148,7 @@ class FallbackDiscovery {
|
|||
assumedWsUri = Uri.parse('ws://localhost:$hostPort/ws');
|
||||
} on Exception catch (err) {
|
||||
_logger.printTrace(err.toString());
|
||||
_logger.printTrace('Failed to connect directly, falling back to mDNS');
|
||||
_logger.printTrace('Failed to connect directly, falling back to log scanning');
|
||||
_sendFailureEvent(err, assumedDevicePort);
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -2,15 +2,20 @@
|
|||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:process/process.dart';
|
||||
|
||||
import '../artifacts.dart';
|
||||
import '../base/common.dart';
|
||||
import '../base/io.dart';
|
||||
import '../base/logger.dart';
|
||||
import '../base/platform.dart';
|
||||
import '../base/process.dart';
|
||||
import '../build_info.dart';
|
||||
import '../cache.dart';
|
||||
import '../convert.dart';
|
||||
import 'code_signing.dart';
|
||||
import 'devices.dart';
|
||||
|
||||
|
@ -107,10 +112,47 @@ class IOSDeploy {
|
|||
);
|
||||
}
|
||||
|
||||
/// Returns [IOSDeployDebugger] wrapping attached debugger logic.
|
||||
///
|
||||
/// This method does not install the app. Call [IOSDeployDebugger.launchAndAttach()]
|
||||
/// to install and attach the debugger to the specified app bundle.
|
||||
IOSDeployDebugger prepareDebuggerForLaunch({
|
||||
@required String deviceId,
|
||||
@required String bundlePath,
|
||||
@required List<String> launchArguments,
|
||||
@required IOSDeviceInterface interfaceType,
|
||||
}) {
|
||||
// Interactive debug session to support sending the lldb detach command.
|
||||
final List<String> launchCommand = <String>[
|
||||
'script',
|
||||
'-t',
|
||||
'0',
|
||||
'/dev/null',
|
||||
_binaryPath,
|
||||
'--id',
|
||||
deviceId,
|
||||
'--bundle',
|
||||
bundlePath,
|
||||
'--debug',
|
||||
if (interfaceType != IOSDeviceInterface.network)
|
||||
'--no-wifi',
|
||||
if (launchArguments.isNotEmpty) ...<String>[
|
||||
'--args',
|
||||
launchArguments.join(' '),
|
||||
],
|
||||
];
|
||||
return IOSDeployDebugger(
|
||||
launchCommand: launchCommand,
|
||||
logger: _logger,
|
||||
processUtils: _processUtils,
|
||||
iosDeployEnv: iosDeployEnv,
|
||||
);
|
||||
}
|
||||
|
||||
/// Installs and then runs the specified app bundle.
|
||||
///
|
||||
/// Uses ios-deploy and returns the exit code.
|
||||
Future<int> runApp({
|
||||
Future<int> launchApp({
|
||||
@required String deviceId,
|
||||
@required String bundlePath,
|
||||
@required List<String> launchArguments,
|
||||
|
@ -169,30 +211,202 @@ class IOSDeploy {
|
|||
return true;
|
||||
}
|
||||
|
||||
// Maps stdout line stream. Must return original line.
|
||||
String _monitorFailure(String stdout) {
|
||||
// Installation issues.
|
||||
if (stdout.contains(noProvisioningProfileErrorOne) || stdout.contains(noProvisioningProfileErrorTwo)) {
|
||||
_logger.printError(noProvisioningProfileInstruction, emphasis: true);
|
||||
String _monitorFailure(String stdout) => _monitorIOSDeployFailure(stdout, _logger);
|
||||
}
|
||||
|
||||
/// lldb attach state flow.
|
||||
enum _IOSDeployDebuggerState {
|
||||
detached,
|
||||
launching,
|
||||
attached,
|
||||
}
|
||||
|
||||
/// Wrapper to launch app and attach the debugger with ios-deploy.
|
||||
class IOSDeployDebugger {
|
||||
IOSDeployDebugger({
|
||||
@required Logger logger,
|
||||
@required ProcessUtils processUtils,
|
||||
@required List<String> launchCommand,
|
||||
@required Map<String, String> iosDeployEnv,
|
||||
}) : _processUtils = processUtils,
|
||||
_logger = logger,
|
||||
_launchCommand = launchCommand,
|
||||
_iosDeployEnv = iosDeployEnv,
|
||||
_debuggerState = _IOSDeployDebuggerState.detached;
|
||||
|
||||
/// Create a [IOSDeployDebugger] for testing.
|
||||
///
|
||||
/// Sets the command to "ios-deploy" and environment to an empty map.
|
||||
@visibleForTesting
|
||||
factory IOSDeployDebugger.test({
|
||||
@required ProcessManager processManager,
|
||||
Logger logger,
|
||||
}) {
|
||||
final Logger debugLogger = logger ?? BufferLogger.test();
|
||||
return IOSDeployDebugger(
|
||||
logger: debugLogger,
|
||||
processUtils: ProcessUtils(logger: debugLogger, processManager: processManager),
|
||||
launchCommand: <String>['ios-deploy'],
|
||||
iosDeployEnv: <String, String>{},
|
||||
);
|
||||
}
|
||||
|
||||
final Logger _logger;
|
||||
final ProcessUtils _processUtils;
|
||||
final List<String> _launchCommand;
|
||||
final Map<String, String> _iosDeployEnv;
|
||||
|
||||
Process _iosDeployProcess;
|
||||
|
||||
Stream<String> get logLines => _debuggerOutput.stream;
|
||||
final StreamController<String> _debuggerOutput = StreamController<String>.broadcast();
|
||||
|
||||
bool get debuggerAttached => _debuggerState == _IOSDeployDebuggerState.attached;
|
||||
_IOSDeployDebuggerState _debuggerState;
|
||||
|
||||
// (lldb) run
|
||||
// https://github.com/ios-control/ios-deploy/blob/1.11.2-beta.1/src/ios-deploy/ios-deploy.m#L51
|
||||
static final RegExp _lldbRun = RegExp(r'\(lldb\)\s*run');
|
||||
|
||||
/// Launch the app on the device, and attach the debugger.
|
||||
///
|
||||
/// Returns whether or not the debugger successfully attached.
|
||||
Future<bool> launchAndAttach() async {
|
||||
// Return when the debugger attaches, or the ios-deploy process exits.
|
||||
final Completer<bool> debuggerCompleter = Completer<bool>();
|
||||
try {
|
||||
_iosDeployProcess = await _processUtils.start(
|
||||
_launchCommand,
|
||||
environment: _iosDeployEnv,
|
||||
);
|
||||
String lastLineFromDebugger;
|
||||
final StreamSubscription<String> stdoutSubscription = _iosDeployProcess.stdout
|
||||
.transform<String>(utf8.decoder)
|
||||
.transform<String>(const LineSplitter())
|
||||
.listen((String line) {
|
||||
_monitorIOSDeployFailure(line, _logger);
|
||||
|
||||
// (lldb) run
|
||||
// success
|
||||
// 2020-09-15 13:42:25.185474-0700 Runner[477:181141] flutter: Observatory listening on http://127.0.0.1:57782/
|
||||
if (_lldbRun.hasMatch(line)) {
|
||||
_logger.printTrace(line);
|
||||
_debuggerState = _IOSDeployDebuggerState.launching;
|
||||
return;
|
||||
}
|
||||
// Next line after "run" must be "success", or the attach failed.
|
||||
// Example: "error: process launch failed"
|
||||
if (_debuggerState == _IOSDeployDebuggerState.launching) {
|
||||
_logger.printTrace(line);
|
||||
final bool attachSuccess = line == 'success';
|
||||
_debuggerState = attachSuccess ? _IOSDeployDebuggerState.attached : _IOSDeployDebuggerState.detached;
|
||||
if (!debuggerCompleter.isCompleted) {
|
||||
debuggerCompleter.complete(attachSuccess);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (line.contains('PROCESS_STOPPED') ||
|
||||
line.contains('PROCESS_EXITED')) {
|
||||
// The app exited or crashed, so stop echoing the output.
|
||||
// Don't pass any further ios-deploy debugging messages to the log reader after it exits.
|
||||
_debuggerState = _IOSDeployDebuggerState.detached;
|
||||
_logger.printTrace(line);
|
||||
return;
|
||||
}
|
||||
if (_debuggerState != _IOSDeployDebuggerState.attached) {
|
||||
_logger.printTrace(line);
|
||||
return;
|
||||
}
|
||||
if (lastLineFromDebugger != null && lastLineFromDebugger.isNotEmpty && line.isEmpty) {
|
||||
// The lldb console stream from ios-deploy is separated lines by an extra \r\n.
|
||||
// To avoid all lines being double spaced, if the last line from the
|
||||
// debugger was not an empty line, skip this empty line.
|
||||
// This will still cause "legit" logged newlines to be doubled...
|
||||
} else {
|
||||
_debuggerOutput.add(line);
|
||||
}
|
||||
lastLineFromDebugger = line;
|
||||
});
|
||||
final StreamSubscription<String> stderrSubscription = _iosDeployProcess.stderr
|
||||
.transform<String>(utf8.decoder)
|
||||
.transform<String>(const LineSplitter())
|
||||
.listen((String line) {
|
||||
_monitorIOSDeployFailure(line, _logger);
|
||||
_logger.printTrace(line);
|
||||
});
|
||||
unawaited(_iosDeployProcess.exitCode.then((int status) {
|
||||
_logger.printTrace('ios-deploy exited with code $exitCode');
|
||||
_debuggerState = _IOSDeployDebuggerState.detached;
|
||||
unawaited(stdoutSubscription.cancel());
|
||||
unawaited(stderrSubscription.cancel());
|
||||
}).whenComplete(() async {
|
||||
if (_debuggerOutput.hasListener) {
|
||||
// Tell listeners the process died.
|
||||
await _debuggerOutput.close();
|
||||
}
|
||||
if (!debuggerCompleter.isCompleted) {
|
||||
debuggerCompleter.complete(false);
|
||||
}
|
||||
_iosDeployProcess = null;
|
||||
}));
|
||||
} on ProcessException catch (exception, stackTrace) {
|
||||
_logger.printTrace('ios-deploy failed: $exception');
|
||||
_debuggerState = _IOSDeployDebuggerState.detached;
|
||||
_debuggerOutput.addError(exception, stackTrace);
|
||||
} on ArgumentError catch (exception, stackTrace) {
|
||||
_logger.printTrace('ios-deploy failed: $exception');
|
||||
_debuggerState = _IOSDeployDebuggerState.detached;
|
||||
_debuggerOutput.addError(exception, stackTrace);
|
||||
}
|
||||
// Wait until the debugger attaches, or the attempt fails.
|
||||
return debuggerCompleter.future;
|
||||
}
|
||||
|
||||
bool exit() {
|
||||
final bool success = (_iosDeployProcess == null) || _iosDeployProcess.kill();
|
||||
_iosDeployProcess = null;
|
||||
return success;
|
||||
}
|
||||
|
||||
void detach() {
|
||||
if (!debuggerAttached) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Detach lldb from the app process.
|
||||
_iosDeployProcess?.stdin?.writeln('process detach');
|
||||
_debuggerState = _IOSDeployDebuggerState.detached;
|
||||
} on SocketException catch (error) {
|
||||
// Best effort, try to detach, but maybe the app already exited or already detached.
|
||||
_logger.printTrace('Could not detach from debugger: $error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Maps stdout line stream. Must return original line.
|
||||
String _monitorIOSDeployFailure(String stdout, Logger logger) {
|
||||
// Installation issues.
|
||||
if (stdout.contains(noProvisioningProfileErrorOne) || stdout.contains(noProvisioningProfileErrorTwo)) {
|
||||
logger.printError(noProvisioningProfileInstruction, emphasis: true);
|
||||
|
||||
// Launch issues.
|
||||
} else if (stdout.contains(deviceLockedError)) {
|
||||
_logger.printError('''
|
||||
} else if (stdout.contains(deviceLockedError)) {
|
||||
logger.printError('''
|
||||
═══════════════════════════════════════════════════════════════════════════════════
|
||||
Your device is locked. Unlock your device first before running.
|
||||
═══════════════════════════════════════════════════════════════════════════════════''',
|
||||
emphasis: true);
|
||||
} else if (stdout.contains(unknownAppLaunchError)) {
|
||||
_logger.printError('''
|
||||
emphasis: true);
|
||||
} else if (stdout.contains(unknownAppLaunchError)) {
|
||||
logger.printError('''
|
||||
═══════════════════════════════════════════════════════════════════════════════════
|
||||
Error launching app. Try launching from within Xcode via:
|
||||
open ios/Runner.xcworkspace
|
||||
|
||||
Your Xcode version may be too old for your iOS version.
|
||||
═══════════════════════════════════════════════════════════════════════════════════''',
|
||||
emphasis: true);
|
||||
}
|
||||
|
||||
return stdout;
|
||||
emphasis: true);
|
||||
}
|
||||
|
||||
return stdout;
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ class ProtocolDiscovery {
|
|||
factory ProtocolDiscovery.observatory(
|
||||
DeviceLogReader logReader, {
|
||||
DevicePortForwarder portForwarder,
|
||||
Duration throttleDuration = const Duration(milliseconds: 200),
|
||||
Duration throttleDuration,
|
||||
Duration throttleTimeout,
|
||||
@required int hostPort,
|
||||
@required int devicePort,
|
||||
|
@ -45,7 +45,7 @@ class ProtocolDiscovery {
|
|||
logReader,
|
||||
kObservatoryService,
|
||||
portForwarder: portForwarder,
|
||||
throttleDuration: throttleDuration,
|
||||
throttleDuration: throttleDuration ?? const Duration(milliseconds: 200),
|
||||
throttleTimeout: throttleTimeout,
|
||||
hostPort: hostPort,
|
||||
devicePort: devicePort,
|
||||
|
@ -225,7 +225,7 @@ class _BufferedStreamController<T> {
|
|||
///
|
||||
/// For example, consider a `waitDuration` of `10ms`, and list of event names
|
||||
/// and arrival times: `a (0ms), b (5ms), c (11ms), d (21ms)`.
|
||||
/// The events `c` and `d` will be produced as a result.
|
||||
/// The events `a`, `c`, and `d` will be produced as a result.
|
||||
StreamTransformer<S, S> _throttle<S>({
|
||||
@required Duration waitDuration,
|
||||
}) {
|
||||
|
@ -240,10 +240,13 @@ StreamTransformer<S, S> _throttle<S>({
|
|||
handleData: (S value, EventSink<S> sink) {
|
||||
latestLine = value;
|
||||
|
||||
final bool isFirstMessage = lastExecution == null;
|
||||
final int currentTime = DateTime.now().millisecondsSinceEpoch;
|
||||
lastExecution ??= currentTime;
|
||||
final int remainingTime = currentTime - lastExecution;
|
||||
final int nextExecutionTime = remainingTime > waitDuration.inMilliseconds
|
||||
|
||||
// Always send the first event immediately.
|
||||
final int nextExecutionTime = isFirstMessage || remainingTime > waitDuration.inMilliseconds
|
||||
? 0
|
||||
: waitDuration.inMilliseconds - remainingTime;
|
||||
|
||||
|
|
|
@ -2,10 +2,15 @@
|
|||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_tools/src/artifacts.dart';
|
||||
import 'package:flutter_tools/src/base/file_system.dart';
|
||||
import 'package:flutter_tools/src/base/logger.dart';
|
||||
import 'package:flutter_tools/src/base/platform.dart';
|
||||
import 'package:flutter_tools/src/cache.dart';
|
||||
import 'package:flutter_tools/src/ios/devices.dart';
|
||||
import 'package:flutter_tools/src/ios/ios_deploy.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:process/process.dart';
|
||||
|
@ -21,50 +26,230 @@ void main () {
|
|||
expect(environment['PATH'], startsWith('/usr/bin'));
|
||||
});
|
||||
|
||||
testWithoutContext('IOSDeploy.uninstallApp calls ios-deploy with correct arguments and returns 0 on success', () async {
|
||||
const String deviceId = '123';
|
||||
const String bundleId = 'com.example.app';
|
||||
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
|
||||
const FakeCommand(command: <String>[
|
||||
'ios-deploy',
|
||||
'--id',
|
||||
deviceId,
|
||||
'--uninstall_only',
|
||||
'--bundle_id',
|
||||
bundleId,
|
||||
])
|
||||
]);
|
||||
final IOSDeploy iosDeploy = setUpIOSDeploy(processManager);
|
||||
final int exitCode = await iosDeploy.uninstallApp(
|
||||
deviceId: deviceId,
|
||||
bundleId: bundleId,
|
||||
);
|
||||
group('IOSDeploy.prepareDebuggerForLaunch', () {
|
||||
testWithoutContext('calls ios-deploy with correct arguments and returns when debugger attaches', () async {
|
||||
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
|
||||
FakeCommand(
|
||||
command: <String>[
|
||||
'script',
|
||||
'-t',
|
||||
'0',
|
||||
'/dev/null',
|
||||
'ios-deploy',
|
||||
'--id',
|
||||
'123',
|
||||
'--bundle',
|
||||
'/',
|
||||
'--debug',
|
||||
'--args',
|
||||
<String>[
|
||||
'--enable-dart-profiling',
|
||||
].join(' '),
|
||||
], environment: const <String, String>{
|
||||
'PATH': '/usr/bin:/usr/local/bin:/usr/bin',
|
||||
'DYLD_LIBRARY_PATH': '/path/to/libs',
|
||||
},
|
||||
stdout: '(lldb) run\nsuccess\nDid finish launching.',
|
||||
),
|
||||
]);
|
||||
final IOSDeploy iosDeploy = setUpIOSDeploy(processManager);
|
||||
final IOSDeployDebugger iosDeployDebugger = iosDeploy.prepareDebuggerForLaunch(
|
||||
deviceId: '123',
|
||||
bundlePath: '/',
|
||||
launchArguments: <String>['--enable-dart-profiling'],
|
||||
interfaceType: IOSDeviceInterface.network,
|
||||
);
|
||||
|
||||
expect(exitCode, 0);
|
||||
expect(processManager.hasRemainingExpectations, false);
|
||||
expect(await iosDeployDebugger.launchAndAttach(), isTrue);
|
||||
expect(await iosDeployDebugger.logLines.toList(), <String>['Did finish launching.']);
|
||||
expect(processManager.hasRemainingExpectations, false);
|
||||
});
|
||||
});
|
||||
|
||||
testWithoutContext('IOSDeploy.uninstallApp returns non-zero exit code when ios-deploy does the same', () async {
|
||||
const String deviceId = '123';
|
||||
const String bundleId = 'com.example.app';
|
||||
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
|
||||
const FakeCommand(command: <String>[
|
||||
'ios-deploy',
|
||||
'--id',
|
||||
deviceId,
|
||||
'--uninstall_only',
|
||||
'--bundle_id',
|
||||
bundleId,
|
||||
], exitCode: 1)
|
||||
]);
|
||||
final IOSDeploy iosDeploy = setUpIOSDeploy(processManager);
|
||||
final int exitCode = await iosDeploy.uninstallApp(
|
||||
deviceId: deviceId,
|
||||
bundleId: bundleId,
|
||||
);
|
||||
group('IOSDeployDebugger', () {
|
||||
group('launch', () {
|
||||
BufferLogger logger;
|
||||
|
||||
expect(exitCode, 1);
|
||||
expect(processManager.hasRemainingExpectations, false);
|
||||
setUp(() {
|
||||
logger = BufferLogger.test();
|
||||
});
|
||||
|
||||
testWithoutContext('debugger attached', () async {
|
||||
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
|
||||
const FakeCommand(
|
||||
command: <String>['ios-deploy'],
|
||||
stdout: '(lldb) run\r\nsuccess\r\nsuccess\r\nLog on attach1\r\n\r\nLog on attach2\r\n\r\n\r\n\r\nPROCESS_STOPPED\r\nLog after process exit',
|
||||
),
|
||||
]);
|
||||
final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test(
|
||||
processManager: processManager,
|
||||
logger: logger,
|
||||
);
|
||||
final List<String> receivedLogLines = <String>[];
|
||||
final Stream<String> logLines = iosDeployDebugger.logLines
|
||||
..listen(receivedLogLines.add);
|
||||
|
||||
expect(await iosDeployDebugger.launchAndAttach(), isTrue);
|
||||
await logLines.toList();
|
||||
expect(receivedLogLines, <String>[
|
||||
'success', // ignore first "success" from lldb, but log subsequent ones from real logging.
|
||||
'Log on attach1',
|
||||
'Log on attach2',
|
||||
'', '']);
|
||||
});
|
||||
|
||||
testWithoutContext('attach failed', () async {
|
||||
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
|
||||
const FakeCommand(
|
||||
command: <String>['ios-deploy'],
|
||||
// A success after an error should never happen, but test that we're handling random "successes" anyway.
|
||||
stdout: '(lldb) run\r\nerror: process launch failed\r\nsuccess\r\nLog on attach1',
|
||||
),
|
||||
]);
|
||||
final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test(
|
||||
processManager: processManager,
|
||||
logger: logger,
|
||||
);
|
||||
final List<String> receivedLogLines = <String>[];
|
||||
final Stream<String> logLines = iosDeployDebugger.logLines
|
||||
..listen(receivedLogLines.add);
|
||||
|
||||
expect(await iosDeployDebugger.launchAndAttach(), isFalse);
|
||||
await logLines.toList();
|
||||
// Debugger lines are double spaced, separated by an extra \r\n. Skip the extra lines.
|
||||
// Still include empty lines other than the extra added newlines.
|
||||
expect(receivedLogLines, isEmpty);
|
||||
});
|
||||
|
||||
testWithoutContext('no provisioning profile 1, stdout', () async {
|
||||
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
|
||||
const FakeCommand(
|
||||
command: <String>['ios-deploy'],
|
||||
stdout: 'Error 0xe8008015',
|
||||
),
|
||||
]);
|
||||
final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test(
|
||||
processManager: processManager,
|
||||
logger: logger,
|
||||
);
|
||||
|
||||
await iosDeployDebugger.launchAndAttach();
|
||||
expect(logger.errorText, contains('No Provisioning Profile was found'));
|
||||
});
|
||||
|
||||
testWithoutContext('no provisioning profile 2, stderr', () async {
|
||||
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
|
||||
const FakeCommand(
|
||||
command: <String>['ios-deploy'],
|
||||
stderr: 'Error 0xe8000067',
|
||||
),
|
||||
]);
|
||||
final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test(
|
||||
processManager: processManager,
|
||||
logger: logger,
|
||||
);
|
||||
await iosDeployDebugger.launchAndAttach();
|
||||
expect(logger.errorText, contains('No Provisioning Profile was found'));
|
||||
});
|
||||
|
||||
testWithoutContext('device locked', () async {
|
||||
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
|
||||
const FakeCommand(
|
||||
command: <String>['ios-deploy'],
|
||||
stdout: 'e80000e2',
|
||||
),
|
||||
]);
|
||||
final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test(
|
||||
processManager: processManager,
|
||||
logger: logger,
|
||||
);
|
||||
await iosDeployDebugger.launchAndAttach();
|
||||
expect(logger.errorText, contains('Your device is locked.'));
|
||||
});
|
||||
|
||||
testWithoutContext('device locked', () async {
|
||||
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
|
||||
const FakeCommand(
|
||||
command: <String>['ios-deploy'],
|
||||
stdout: 'Error 0xe8000022',
|
||||
),
|
||||
]);
|
||||
final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test(
|
||||
processManager: processManager,
|
||||
logger: logger,
|
||||
);
|
||||
await iosDeployDebugger.launchAndAttach();
|
||||
expect(logger.errorText, contains('Try launching from within Xcode'));
|
||||
});
|
||||
});
|
||||
|
||||
testWithoutContext('detach', () async {
|
||||
final StreamController<List<int>> stdin = StreamController<List<int>>();
|
||||
final Stream<String> stdinStream = stdin.stream.transform<String>(const Utf8Decoder());
|
||||
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
|
||||
FakeCommand(
|
||||
command: const <String>[
|
||||
'ios-deploy',
|
||||
],
|
||||
stdout: '(lldb) run\nsuccess',
|
||||
stdin: IOSink(stdin.sink),
|
||||
),
|
||||
]);
|
||||
final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test(
|
||||
processManager: processManager,
|
||||
);
|
||||
await iosDeployDebugger.launchAndAttach();
|
||||
iosDeployDebugger.detach();
|
||||
expect(await stdinStream.first, 'process detach');
|
||||
});
|
||||
});
|
||||
|
||||
group('IOSDeploy.uninstallApp', () {
|
||||
testWithoutContext('calls ios-deploy with correct arguments and returns 0 on success', () async {
|
||||
const String deviceId = '123';
|
||||
const String bundleId = 'com.example.app';
|
||||
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
|
||||
const FakeCommand(command: <String>[
|
||||
'ios-deploy',
|
||||
'--id',
|
||||
deviceId,
|
||||
'--uninstall_only',
|
||||
'--bundle_id',
|
||||
bundleId,
|
||||
])
|
||||
]);
|
||||
final IOSDeploy iosDeploy = setUpIOSDeploy(processManager);
|
||||
final int exitCode = await iosDeploy.uninstallApp(
|
||||
deviceId: deviceId,
|
||||
bundleId: bundleId,
|
||||
);
|
||||
|
||||
expect(exitCode, 0);
|
||||
expect(processManager.hasRemainingExpectations, false);
|
||||
});
|
||||
|
||||
testWithoutContext('returns non-zero exit code when ios-deploy does the same', () async {
|
||||
const String deviceId = '123';
|
||||
const String bundleId = 'com.example.app';
|
||||
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
|
||||
const FakeCommand(command: <String>[
|
||||
'ios-deploy',
|
||||
'--id',
|
||||
deviceId,
|
||||
'--uninstall_only',
|
||||
'--bundle_id',
|
||||
bundleId,
|
||||
], exitCode: 1)
|
||||
]);
|
||||
final IOSDeploy iosDeploy = setUpIOSDeploy(processManager);
|
||||
final int exitCode = await iosDeploy.uninstallApp(
|
||||
deviceId: deviceId,
|
||||
bundleId: bundleId,
|
||||
);
|
||||
|
||||
expect(exitCode, 1);
|
||||
expect(processManager.hasRemainingExpectations, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import 'package:flutter_tools/src/build_info.dart';
|
|||
import 'package:flutter_tools/src/convert.dart';
|
||||
import 'package:flutter_tools/src/device.dart';
|
||||
import 'package:flutter_tools/src/ios/devices.dart';
|
||||
import 'package:flutter_tools/src/ios/ios_deploy.dart';
|
||||
import 'package:flutter_tools/src/ios/mac.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:vm_service/vm_service.dart';
|
||||
|
@ -30,167 +31,299 @@ void main() {
|
|||
artifacts = MockArtifacts();
|
||||
logger = BufferLogger.test();
|
||||
when(artifacts.getArtifactPath(Artifact.idevicesyslog, platform: TargetPlatform.ios))
|
||||
.thenReturn('idevice-syslog');
|
||||
.thenReturn('idevice-syslog');
|
||||
});
|
||||
|
||||
testWithoutContext('decodeSyslog decodes a syslog-encoded line', () {
|
||||
final String decoded = decodeSyslog(
|
||||
r'I \M-b\M^]\M-$\M-o\M-8\M^O syslog \M-B\M-/\'
|
||||
r'134_(\M-c\M^C\M^D)_/\M-B\M-/ \M-l\M^F\240!');
|
||||
group('syslog stream', () {
|
||||
testWithoutContext('decodeSyslog decodes a syslog-encoded line', () {
|
||||
final String decoded = decodeSyslog(
|
||||
r'I \M-b\M^]\M-$\M-o\M-8\M^O syslog \M-B\M-/\'
|
||||
r'134_(\M-c\M^C\M^D)_/\M-B\M-/ \M-l\M^F\240!');
|
||||
|
||||
expect(decoded, r'I ❤️ syslog ¯\_(ツ)_/¯ 솠!');
|
||||
});
|
||||
expect(decoded, r'I ❤️ syslog ¯\_(ツ)_/¯ 솠!');
|
||||
});
|
||||
|
||||
testWithoutContext('decodeSyslog passes through un-decodeable lines as-is', () {
|
||||
final String decoded = decodeSyslog(r'I \M-b\M^O syslog!');
|
||||
testWithoutContext('decodeSyslog passes through un-decodeable lines as-is', () {
|
||||
final String decoded = decodeSyslog(r'I \M-b\M^O syslog!');
|
||||
|
||||
expect(decoded, r'I \M-b\M^O syslog!');
|
||||
});
|
||||
expect(decoded, r'I \M-b\M^O syslog!');
|
||||
});
|
||||
|
||||
testWithoutContext('IOSDeviceLogReader suppresses non-Flutter lines from output with syslog', () async {
|
||||
processManager.addCommand(
|
||||
const FakeCommand(
|
||||
command: <String>[
|
||||
'idevice-syslog', '-u', '1234',
|
||||
],
|
||||
stdout: '''
|
||||
testWithoutContext('IOSDeviceLogReader suppresses non-Flutter lines from output with syslog', () async {
|
||||
processManager.addCommand(
|
||||
const FakeCommand(
|
||||
command: <String>[
|
||||
'idevice-syslog', '-u', '1234',
|
||||
],
|
||||
stdout: '''
|
||||
Runner(Flutter)[297] <Notice>: A is for ari
|
||||
Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt MobileGestaltSupport.m:153: pid 123 (Runner) does not have sandbox access for frZQaeyWLUvLjeuEK43hmg and IS NOT appropriately entitled
|
||||
Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt MobileGestalt.c:550: no access to InverseDeviceID (see <rdar://problem/11744455>)
|
||||
Runner(Flutter)[297] <Notice>: I is for ichigo
|
||||
Runner(UIKit)[297] <Notice>: E is for enpitsu"
|
||||
'''
|
||||
),
|
||||
);
|
||||
final DeviceLogReader logReader = IOSDeviceLogReader.test(
|
||||
iMobileDevice: IMobileDevice(
|
||||
artifacts: artifacts,
|
||||
processManager: processManager,
|
||||
cache: fakeCache,
|
||||
logger: logger,
|
||||
),
|
||||
);
|
||||
final List<String> lines = await logReader.logLines.toList();
|
||||
),
|
||||
);
|
||||
final DeviceLogReader logReader = IOSDeviceLogReader.test(
|
||||
iMobileDevice: IMobileDevice(
|
||||
artifacts: artifacts,
|
||||
processManager: processManager,
|
||||
cache: fakeCache,
|
||||
logger: logger,
|
||||
),
|
||||
);
|
||||
final List<String> lines = await logReader.logLines.toList();
|
||||
|
||||
expect(lines, <String>['A is for ari', 'I is for ichigo']);
|
||||
});
|
||||
expect(lines, <String>['A is for ari', 'I is for ichigo']);
|
||||
});
|
||||
|
||||
testWithoutContext('IOSDeviceLogReader includes multi-line Flutter logs in the output with syslog', () async {
|
||||
processManager.addCommand(
|
||||
const FakeCommand(
|
||||
command: <String>[
|
||||
'idevice-syslog', '-u', '1234',
|
||||
],
|
||||
stdout: '''
|
||||
testWithoutContext('IOSDeviceLogReader includes multi-line Flutter logs in the output with syslog', () async {
|
||||
processManager.addCommand(
|
||||
const FakeCommand(
|
||||
command: <String>[
|
||||
'idevice-syslog', '-u', '1234',
|
||||
],
|
||||
stdout: '''
|
||||
Runner(Flutter)[297] <Notice>: This is a multi-line message,
|
||||
with another Flutter message following it.
|
||||
Runner(Flutter)[297] <Notice>: This is a multi-line message,
|
||||
with a non-Flutter log message following it.
|
||||
Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt
|
||||
'''
|
||||
),
|
||||
);
|
||||
final DeviceLogReader logReader = IOSDeviceLogReader.test(
|
||||
iMobileDevice: IMobileDevice(
|
||||
artifacts: artifacts,
|
||||
processManager: processManager,
|
||||
cache: fakeCache,
|
||||
logger: logger,
|
||||
),
|
||||
);
|
||||
final List<String> lines = await logReader.logLines.toList();
|
||||
),
|
||||
);
|
||||
final DeviceLogReader logReader = IOSDeviceLogReader.test(
|
||||
iMobileDevice: IMobileDevice(
|
||||
artifacts: artifacts,
|
||||
processManager: processManager,
|
||||
cache: fakeCache,
|
||||
logger: logger,
|
||||
),
|
||||
);
|
||||
final List<String> lines = await logReader.logLines.toList();
|
||||
|
||||
expect(lines, <String>[
|
||||
'This is a multi-line message,',
|
||||
' with another Flutter message following it.',
|
||||
'This is a multi-line message,',
|
||||
' with a non-Flutter log message following it.',
|
||||
]);
|
||||
});
|
||||
expect(lines, <String>[
|
||||
'This is a multi-line message,',
|
||||
' with another Flutter message following it.',
|
||||
'This is a multi-line message,',
|
||||
' with a non-Flutter log message following it.',
|
||||
]);
|
||||
});
|
||||
|
||||
testWithoutContext('includes multi-line Flutter logs in the output', () async {
|
||||
processManager.addCommand(
|
||||
const FakeCommand(
|
||||
command: <String>[
|
||||
'idevice-syslog', '-u', '1234',
|
||||
],
|
||||
stdout: '''
|
||||
testWithoutContext('includes multi-line Flutter logs in the output', () async {
|
||||
processManager.addCommand(
|
||||
const FakeCommand(
|
||||
command: <String>[
|
||||
'idevice-syslog', '-u', '1234',
|
||||
],
|
||||
stdout: '''
|
||||
Runner(Flutter)[297] <Notice>: This is a multi-line message,
|
||||
with another Flutter message following it.
|
||||
Runner(Flutter)[297] <Notice>: This is a multi-line message,
|
||||
with a non-Flutter log message following it.
|
||||
Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt
|
||||
''',
|
||||
),
|
||||
);
|
||||
),
|
||||
);
|
||||
|
||||
final DeviceLogReader logReader = IOSDeviceLogReader.test(
|
||||
iMobileDevice: IMobileDevice(
|
||||
artifacts: artifacts,
|
||||
processManager: processManager,
|
||||
cache: fakeCache,
|
||||
logger: logger,
|
||||
),
|
||||
);
|
||||
final List<String> lines = await logReader.logLines.toList();
|
||||
final DeviceLogReader logReader = IOSDeviceLogReader.test(
|
||||
iMobileDevice: IMobileDevice(
|
||||
artifacts: artifacts,
|
||||
processManager: processManager,
|
||||
cache: fakeCache,
|
||||
logger: logger,
|
||||
),
|
||||
);
|
||||
final List<String> lines = await logReader.logLines.toList();
|
||||
|
||||
expect(lines, <String>[
|
||||
'This is a multi-line message,',
|
||||
' with another Flutter message following it.',
|
||||
'This is a multi-line message,',
|
||||
' with a non-Flutter log message following it.',
|
||||
]);
|
||||
expect(lines, <String>[
|
||||
'This is a multi-line message,',
|
||||
' with another Flutter message following it.',
|
||||
'This is a multi-line message,',
|
||||
' with a non-Flutter log message following it.',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
testWithoutContext('IOSDeviceLogReader can listen to VM Service logs', () async {
|
||||
final MockVmService vmService = MockVmService();
|
||||
final DeviceLogReader logReader = IOSDeviceLogReader.test(
|
||||
useSyslog: false,
|
||||
iMobileDevice: IMobileDevice(
|
||||
artifacts: artifacts,
|
||||
processManager: processManager,
|
||||
cache: fakeCache,
|
||||
logger: logger,
|
||||
),
|
||||
);
|
||||
final StreamController<Event> stdoutController = StreamController<Event>();
|
||||
final StreamController<Event> stderController = StreamController<Event>();
|
||||
final Completer<Success> stdoutCompleter = Completer<Success>();
|
||||
final Completer<Success> stderrCompleter = Completer<Success>();
|
||||
when(vmService.streamListen('Stdout')).thenAnswer((Invocation invocation) {
|
||||
return stdoutCompleter.future;
|
||||
});
|
||||
when(vmService.streamListen('Stderr')).thenAnswer((Invocation invocation) {
|
||||
return stderrCompleter.future;
|
||||
});
|
||||
when(vmService.onStdoutEvent).thenAnswer((Invocation invocation) {
|
||||
return stdoutController.stream;
|
||||
});
|
||||
when(vmService.onStderrEvent).thenAnswer((Invocation invocation) {
|
||||
return stderController.stream;
|
||||
});
|
||||
logReader.connectedVMService = vmService;
|
||||
group('VM service', () {
|
||||
testWithoutContext('IOSDeviceLogReader can listen to VM Service logs', () async {
|
||||
final MockVmService vmService = MockVmService();
|
||||
final DeviceLogReader logReader = IOSDeviceLogReader.test(
|
||||
useSyslog: false,
|
||||
iMobileDevice: IMobileDevice(
|
||||
artifacts: artifacts,
|
||||
processManager: processManager,
|
||||
cache: fakeCache,
|
||||
logger: logger,
|
||||
),
|
||||
);
|
||||
final StreamController<Event> stdoutController = StreamController<Event>();
|
||||
final StreamController<Event> stderController = StreamController<Event>();
|
||||
final Completer<Success> stdoutCompleter = Completer<Success>();
|
||||
final Completer<Success> stderrCompleter = Completer<Success>();
|
||||
when(vmService.streamListen('Stdout')).thenAnswer((Invocation invocation) {
|
||||
return stdoutCompleter.future;
|
||||
});
|
||||
when(vmService.streamListen('Stderr')).thenAnswer((Invocation invocation) {
|
||||
return stderrCompleter.future;
|
||||
});
|
||||
when(vmService.onStdoutEvent).thenAnswer((Invocation invocation) {
|
||||
return stdoutController.stream;
|
||||
});
|
||||
when(vmService.onStderrEvent).thenAnswer((Invocation invocation) {
|
||||
return stderController.stream;
|
||||
});
|
||||
logReader.connectedVMService = vmService;
|
||||
|
||||
stdoutCompleter.complete(Success());
|
||||
stderrCompleter.complete(Success());
|
||||
stdoutController.add(Event(
|
||||
kind: 'Stdout',
|
||||
timestamp: 0,
|
||||
bytes: base64.encode(utf8.encode(' This is a message ')),
|
||||
));
|
||||
stderController.add(Event(
|
||||
kind: 'Stderr',
|
||||
timestamp: 0,
|
||||
bytes: base64.encode(utf8.encode(' And this is an error ')),
|
||||
));
|
||||
stdoutCompleter.complete(Success());
|
||||
stderrCompleter.complete(Success());
|
||||
stdoutController.add(Event(
|
||||
kind: 'Stdout',
|
||||
timestamp: 0,
|
||||
bytes: base64.encode(utf8.encode(' This is a message ')),
|
||||
));
|
||||
stderController.add(Event(
|
||||
kind: 'Stderr',
|
||||
timestamp: 0,
|
||||
bytes: base64.encode(utf8.encode(' And this is an error ')),
|
||||
));
|
||||
|
||||
// Wait for stream listeners to fire.
|
||||
await expectLater(logReader.logLines, emitsInAnyOrder(<Matcher>[
|
||||
equals(' This is a message '),
|
||||
equals(' And this is an error '),
|
||||
]));
|
||||
// Wait for stream listeners to fire.
|
||||
await expectLater(logReader.logLines, emitsInAnyOrder(<Matcher>[
|
||||
equals(' This is a message '),
|
||||
equals(' And this is an error '),
|
||||
]));
|
||||
});
|
||||
|
||||
testWithoutContext('IOSDeviceLogReader ignores VM Service logs when attached to debugger', () async {
|
||||
final MockVmService vmService = MockVmService();
|
||||
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
|
||||
useSyslog: false,
|
||||
iMobileDevice: IMobileDevice(
|
||||
artifacts: artifacts,
|
||||
processManager: processManager,
|
||||
cache: fakeCache,
|
||||
logger: logger,
|
||||
),
|
||||
);
|
||||
final StreamController<Event> stdoutController = StreamController<Event>();
|
||||
final StreamController<Event> stderController = StreamController<Event>();
|
||||
final Completer<Success> stdoutCompleter = Completer<Success>();
|
||||
final Completer<Success> stderrCompleter = Completer<Success>();
|
||||
when(vmService.streamListen('Stdout')).thenAnswer((Invocation invocation) {
|
||||
return stdoutCompleter.future;
|
||||
});
|
||||
when(vmService.streamListen('Stderr')).thenAnswer((Invocation invocation) {
|
||||
return stderrCompleter.future;
|
||||
});
|
||||
when(vmService.onStdoutEvent).thenAnswer((Invocation invocation) {
|
||||
return stdoutController.stream;
|
||||
});
|
||||
when(vmService.onStderrEvent).thenAnswer((Invocation invocation) {
|
||||
return stderController.stream;
|
||||
});
|
||||
logReader.connectedVMService = vmService;
|
||||
|
||||
stdoutCompleter.complete(Success());
|
||||
stderrCompleter.complete(Success());
|
||||
stdoutController.add(Event(
|
||||
kind: 'Stdout',
|
||||
timestamp: 0,
|
||||
bytes: base64.encode(utf8.encode(' This is a message ')),
|
||||
));
|
||||
stderController.add(Event(
|
||||
kind: 'Stderr',
|
||||
timestamp: 0,
|
||||
bytes: base64.encode(utf8.encode(' And this is an error ')),
|
||||
));
|
||||
|
||||
final MockIOSDeployDebugger iosDeployDebugger = MockIOSDeployDebugger();
|
||||
when(iosDeployDebugger.debuggerAttached).thenReturn(true);
|
||||
|
||||
final Stream<String> debuggingLogs = Stream<String>.fromIterable(<String>[
|
||||
'Message from debugger'
|
||||
]);
|
||||
when(iosDeployDebugger.logLines).thenAnswer((Invocation invocation) => debuggingLogs);
|
||||
logReader.debuggerStream = iosDeployDebugger;
|
||||
|
||||
// Wait for stream listeners to fire.
|
||||
await expectLater(logReader.logLines, emitsInAnyOrder(<Matcher>[
|
||||
equals('Message from debugger'),
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
group('debugger stream', () {
|
||||
testWithoutContext('IOSDeviceLogReader removes metadata prefix from lldb output', () async {
|
||||
final Stream<String> debuggingLogs = Stream<String>.fromIterable(<String>[
|
||||
'2020-09-15 19:15:10.931434-0700 Runner[541:226276] Did finish launching.',
|
||||
'2020-09-15 19:15:10.931434-0700 Runner[541:226276] [Category] Did finish launching from logging category.',
|
||||
'stderr from dart',
|
||||
'',
|
||||
]);
|
||||
|
||||
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
|
||||
iMobileDevice: IMobileDevice(
|
||||
artifacts: artifacts,
|
||||
processManager: processManager,
|
||||
cache: fakeCache,
|
||||
logger: logger,
|
||||
),
|
||||
useSyslog: false,
|
||||
);
|
||||
final MockIOSDeployDebugger iosDeployDebugger = MockIOSDeployDebugger();
|
||||
when(iosDeployDebugger.logLines).thenAnswer((Invocation invocation) => debuggingLogs);
|
||||
logReader.debuggerStream = iosDeployDebugger;
|
||||
final Future<List<String>> logLines = logReader.logLines.toList();
|
||||
|
||||
expect(await logLines, <String>[
|
||||
'Did finish launching.',
|
||||
'[Category] Did finish launching from logging category.',
|
||||
'stderr from dart',
|
||||
'',
|
||||
]);
|
||||
});
|
||||
|
||||
testWithoutContext('errors on debugger stream closes log stream', () async {
|
||||
final Stream<String> debuggingLogs = Stream<String>.error('ios-deploy error');
|
||||
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
|
||||
iMobileDevice: IMobileDevice(
|
||||
artifacts: artifacts,
|
||||
processManager: processManager,
|
||||
cache: fakeCache,
|
||||
logger: logger,
|
||||
),
|
||||
useSyslog: false,
|
||||
);
|
||||
final Completer<void> streamComplete = Completer<void>();
|
||||
final MockIOSDeployDebugger iosDeployDebugger = MockIOSDeployDebugger();
|
||||
when(iosDeployDebugger.logLines).thenAnswer((Invocation invocation) => debuggingLogs);
|
||||
logReader.logLines.listen(null, onError: (Object error) => streamComplete.complete());
|
||||
logReader.debuggerStream = iosDeployDebugger;
|
||||
|
||||
await streamComplete.future;
|
||||
});
|
||||
|
||||
testWithoutContext('detaches debugger', () async {
|
||||
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
|
||||
iMobileDevice: IMobileDevice(
|
||||
artifacts: artifacts,
|
||||
processManager: processManager,
|
||||
cache: fakeCache,
|
||||
logger: logger,
|
||||
),
|
||||
useSyslog: false,
|
||||
);
|
||||
final MockIOSDeployDebugger iosDeployDebugger = MockIOSDeployDebugger();
|
||||
when(iosDeployDebugger.logLines).thenAnswer((Invocation invocation) => const Stream<String>.empty());
|
||||
logReader.debuggerStream = iosDeployDebugger;
|
||||
|
||||
logReader.dispose();
|
||||
verify(iosDeployDebugger.detach());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class MockArtifacts extends Mock implements Artifacts {}
|
||||
class MockVmService extends Mock implements VmService {}
|
||||
class MockIOSDeployDebugger extends Mock implements IOSDeployDebugger {}
|
||||
|
|
|
@ -67,19 +67,25 @@ const FakeCommand kLaunchReleaseCommand = FakeCommand(
|
|||
|
||||
// The command used to actually launch the app with args in debug.
|
||||
const FakeCommand kLaunchDebugCommand = FakeCommand(command: <String>[
|
||||
'script',
|
||||
'-t',
|
||||
'0',
|
||||
'/dev/null',
|
||||
'ios-deploy',
|
||||
'--id',
|
||||
'123',
|
||||
'--bundle',
|
||||
'/',
|
||||
'--debug',
|
||||
'--no-wifi',
|
||||
'--justlaunch',
|
||||
'--args',
|
||||
'--enable-dart-profiling --enable-service-port-fallback --disable-service-auth-codes --observatory-port=60700 --enable-checked-mode --verify-entry-points'
|
||||
], environment: <String, String>{
|
||||
'PATH': '/usr/bin:null',
|
||||
'DYLD_LIBRARY_PATH': '/path/to/libraries',
|
||||
});
|
||||
},
|
||||
stdout: '(lldb) run\nsuccess',
|
||||
);
|
||||
|
||||
void main() {
|
||||
// TODO(jonahwilliams): This test doesn't really belong here but
|
||||
|
@ -102,7 +108,7 @@ void main() {
|
|||
});
|
||||
|
||||
// Still uses context for analytics and mDNS.
|
||||
testUsingContext('IOSDevice.startApp succeeds in debug mode via mDNS discovery', () async {
|
||||
testUsingContext('IOSDevice.startApp succeeds in debug mode via mDNS discovery when log reading fails', () async {
|
||||
final FileSystem fileSystem = MemoryFileSystem.test();
|
||||
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
|
||||
kDeployCommand,
|
||||
|
@ -145,6 +151,7 @@ void main() {
|
|||
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
|
||||
platformArgs: <String, dynamic>{},
|
||||
fallbackPollingDelay: Duration.zero,
|
||||
fallbackThrottleTimeout: const Duration(milliseconds: 10),
|
||||
);
|
||||
|
||||
verify(globals.flutterUsage.sendEvent('ios-handshake', 'mdns-success')).called(1);
|
||||
|
@ -157,7 +164,7 @@ void main() {
|
|||
});
|
||||
|
||||
// Still uses context for analytics and mDNS.
|
||||
testUsingContext('IOSDevice.startApp succeeds in debug mode when mDNS fails', () async {
|
||||
testUsingContext('IOSDevice.startApp succeeds in debug mode via log reading', () async {
|
||||
final FileSystem fileSystem = MemoryFileSystem.test();
|
||||
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
|
||||
kDeployCommand,
|
||||
|
@ -183,34 +190,32 @@ void main() {
|
|||
device.portForwarder = const NoOpDevicePortForwarder();
|
||||
device.setLogReader(iosApp, deviceLogReader);
|
||||
|
||||
// Now that the reader is used, start writing messages to it.
|
||||
// Start writing messages to the log reader.
|
||||
Timer.run(() {
|
||||
deviceLogReader.addLine('Foo');
|
||||
deviceLogReader.addLine('Observatory listening on http://127.0.0.1:456');
|
||||
});
|
||||
when(MDnsObservatoryDiscovery.instance.getObservatoryUri(any, any, usesIpv6: anyNamed('usesIpv6')))
|
||||
.thenAnswer((Invocation invocation) async => null);
|
||||
|
||||
final LaunchResult launchResult = await device.startApp(iosApp,
|
||||
prebuiltApplication: true,
|
||||
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
|
||||
platformArgs: <String, dynamic>{},
|
||||
fallbackPollingDelay: Duration.zero,
|
||||
fallbackThrottleTimeout: const Duration(milliseconds: 10),
|
||||
);
|
||||
|
||||
expect(launchResult.started, true);
|
||||
expect(launchResult.hasObservatory, true);
|
||||
verify(globals.flutterUsage.sendEvent('ios-handshake', 'mdns-failure')).called(1);
|
||||
verify(globals.flutterUsage.sendEvent('ios-handshake', 'fallback-success')).called(1);
|
||||
verify(globals.flutterUsage.sendEvent('ios-handshake', 'log-success')).called(1);
|
||||
verifyNever(globals.flutterUsage.sendEvent('ios-handshake', 'mdns-failure'));
|
||||
expect(await device.stopApp(iosApp), false);
|
||||
}, overrides: <Type, Generator>{
|
||||
Usage: () => MockUsage(),
|
||||
MDnsObservatoryDiscovery: () => MockMDnsObservatoryDiscovery(),
|
||||
Usage: () => MockUsage(),
|
||||
});
|
||||
|
||||
// Still uses context for analytics and mDNS.
|
||||
testUsingContext('IOSDevice.startApp fails in debug mode when mDNS fails and '
|
||||
'when Observatory URI is malformed', () async {
|
||||
testUsingContext('IOSDevice.startApp fails in debug mode when Observatory URI is malformed', () async {
|
||||
final FileSystem fileSystem = MemoryFileSystem.test();
|
||||
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
|
||||
kDeployCommand,
|
||||
|
@ -239,16 +244,15 @@ void main() {
|
|||
// Now that the reader is used, start writing messages to it.
|
||||
Timer.run(() {
|
||||
deviceLogReader.addLine('Foo');
|
||||
deviceLogReader.addLine('Observatory listening on http:/:/127.0.0.1:456');
|
||||
deviceLogReader.addLine('Observatory listening on http://127.0.0.1:456abc');
|
||||
});
|
||||
when(MDnsObservatoryDiscovery.instance.getObservatoryUri(any, any, usesIpv6: anyNamed('usesIpv6')))
|
||||
.thenAnswer((Invocation invocation) async => null);
|
||||
|
||||
final LaunchResult launchResult = await device.startApp(iosApp,
|
||||
prebuiltApplication: true,
|
||||
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
|
||||
platformArgs: <String, dynamic>{},
|
||||
fallbackPollingDelay: Duration.zero,
|
||||
// fallbackThrottleTimeout: const Duration(milliseconds: 10),
|
||||
);
|
||||
|
||||
expect(launchResult.started, false);
|
||||
|
@ -259,8 +263,8 @@ void main() {
|
|||
label: anyNamed('label'),
|
||||
value: anyNamed('value'),
|
||||
)).called(1);
|
||||
verify(globals.flutterUsage.sendEvent('ios-handshake', 'log-failure')).called(1);
|
||||
verify(globals.flutterUsage.sendEvent('ios-handshake', 'mdns-failure')).called(1);
|
||||
verify(globals.flutterUsage.sendEvent('ios-handshake', 'fallback-failure')).called(1);
|
||||
}, overrides: <Type, Generator>{
|
||||
MDnsObservatoryDiscovery: () => MockMDnsObservatoryDiscovery(),
|
||||
Usage: () => MockUsage(),
|
||||
|
@ -388,6 +392,7 @@ void main() {
|
|||
debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
|
||||
platformArgs: <String, dynamic>{},
|
||||
fallbackPollingDelay: Duration.zero,
|
||||
fallbackThrottleTimeout: const Duration(milliseconds: 10),
|
||||
);
|
||||
|
||||
expect(launchResult.started, true);
|
||||
|
@ -405,13 +410,17 @@ void main() {
|
|||
kDeployCommand,
|
||||
FakeCommand(
|
||||
command: <String>[
|
||||
'script',
|
||||
'-t',
|
||||
'0',
|
||||
'/dev/null',
|
||||
'ios-deploy',
|
||||
'--id',
|
||||
'123',
|
||||
'--bundle',
|
||||
'/',
|
||||
'--debug',
|
||||
'--no-wifi',
|
||||
'--justlaunch',
|
||||
// The arguments below are determined by what is passed into
|
||||
// the debugging options argument to startApp.
|
||||
'--args',
|
||||
|
@ -436,7 +445,8 @@ void main() {
|
|||
], environment: const <String, String>{
|
||||
'PATH': '/usr/bin:null',
|
||||
'DYLD_LIBRARY_PATH': '/path/to/libraries',
|
||||
}
|
||||
},
|
||||
stdout: '(lldb) run\nsuccess',
|
||||
)
|
||||
]);
|
||||
final IOSDevice device = setUpIOSDevice(
|
||||
|
@ -455,22 +465,15 @@ void main() {
|
|||
bundleName: 'Runner',
|
||||
bundleDir: fileSystem.currentDirectory,
|
||||
);
|
||||
final Uri uri = Uri(
|
||||
scheme: 'http',
|
||||
host: '127.0.0.1',
|
||||
port: 1234,
|
||||
path: 'observatory',
|
||||
);
|
||||
final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
|
||||
|
||||
device.setLogReader(iosApp, FakeDeviceLogReader());
|
||||
device.portForwarder = const NoOpDevicePortForwarder();
|
||||
device.setLogReader(iosApp, deviceLogReader);
|
||||
|
||||
when(MDnsObservatoryDiscovery.instance.getObservatoryUri(
|
||||
any,
|
||||
any,
|
||||
usesIpv6: anyNamed('usesIpv6'),
|
||||
hostVmservicePort: anyNamed('hostVmservicePort')
|
||||
)).thenAnswer((Invocation invocation) async => uri);
|
||||
// Start writing messages to the log reader.
|
||||
Timer.run(() {
|
||||
deviceLogReader.addLine('Observatory listening on http://127.0.0.1:1234');
|
||||
});
|
||||
|
||||
final LaunchResult launchResult = await device.startApp(iosApp,
|
||||
prebuiltApplication: true,
|
||||
|
@ -492,6 +495,7 @@ void main() {
|
|||
),
|
||||
platformArgs: <String, dynamic>{},
|
||||
fallbackPollingDelay: Duration.zero,
|
||||
fallbackThrottleTimeout: const Duration(milliseconds: 10),
|
||||
);
|
||||
|
||||
expect(launchResult.started, true);
|
||||
|
|
Loading…
Reference in a new issue