Unverified Commit 5df4b7db authored by Emmanuel Garcia's avatar Emmanuel Garcia Committed by GitHub

Reland: Attach looks at future observatory URIs (#45307)

parent 6eb0a0e3
......@@ -855,25 +855,9 @@ class _AdbLogReader extends DeviceLogReader {
@override
String get name => device.name;
DateTime _timeOrigin;
DateTime _adbTimestampToDateTime(String adbTimestamp) {
// The adb timestamp format is: mm-dd hours:minutes:seconds.milliseconds
// Dart's DateTime parse function accepts this format so long as we provide
// the year, resulting in:
// yyyy-mm-dd hours:minutes:seconds.milliseconds.
return DateTime.parse('${DateTime.now().year}-$adbTimestamp');
}
void _start() {
// Start the adb logcat process.
final List<String> args = <String>['shell', '-x', 'logcat', '-v', 'time'];
final String lastTimestamp = device.lastLogcatTimestamp;
if (lastTimestamp != null) {
_timeOrigin = _adbTimestampToDateTime(lastTimestamp);
} else {
_timeOrigin = null;
}
// Start the adb logcat process and filter logs by the "flutter" tag.
final List<String> args = <String>['shell', '-x', 'logcat', '-v', 'time', '-s', 'flutter'];
processUtils.start(device.adbCommandForDevice(args)).then<void>((Process process) {
_process = process;
// We expect logcat streams to occasionally contain invalid utf-8,
......@@ -923,18 +907,7 @@ class _AdbLogReader extends DeviceLogReader {
// mm-dd hh:mm:ss.milliseconds Priority/Tag( PID): ....
void _onLine(String line) {
final Match timeMatch = AndroidDevice._timeRegExp.firstMatch(line);
if (timeMatch == null) {
return;
}
if (_timeOrigin != null) {
final String timestamp = timeMatch.group(0);
final DateTime time = _adbTimestampToDateTime(timestamp);
if (!time.isAfter(_timeOrigin)) {
// Ignore log messages before the origin.
return;
}
}
if (line.length == timeMatch.end) {
if (timeMatch == null || line.length == timeMatch.end) {
return;
}
// Chop off the time.
......
......@@ -4,6 +4,8 @@
import 'dart:async';
import 'package:meta/meta.dart';
import '../artifacts.dart';
import '../base/common.dart';
import '../base/context.dart';
......@@ -200,16 +202,14 @@ class AttachCommand extends FlutterCommand {
notifyingLogger: NotifyingLogger(), logToStdout: true)
: null;
Uri observatoryUri;
Stream<Uri> observatoryUri;
bool usesIpv6 = ipv6;
final String ipv6Loopback = InternetAddress.loopbackIPv6.address;
final String ipv4Loopback = InternetAddress.loopbackIPv4.address;
final String hostname = usesIpv6 ? ipv6Loopback : ipv4Loopback;
bool attachLogger = false;
if (devicePort == null && debugUri == null) {
if (device is FuchsiaDevice) {
attachLogger = true;
final String module = stringArg('module');
if (module == null) {
throwToolExit('\'--module\' is required for attaching to a Fuchsia device');
......@@ -218,8 +218,7 @@ class AttachCommand extends FlutterCommand {
FuchsiaIsolateDiscoveryProtocol isolateDiscoveryProtocol;
try {
isolateDiscoveryProtocol = device.getIsolateDiscoveryProtocol(module);
observatoryUri = await isolateDiscoveryProtocol.uri;
printStatus('Done.'); // FYI, this message is used as a sentinel in tests.
observatoryUri = Stream<Uri>.fromFuture(isolateDiscoveryProtocol.uri).asBroadcastStream();
} catch (_) {
isolateDiscoveryProtocol?.dispose();
final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
......@@ -229,85 +228,55 @@ class AttachCommand extends FlutterCommand {
rethrow;
}
} else if ((device is IOSDevice) || (device is IOSSimulator)) {
observatoryUri = await MDnsObservatoryDiscovery.instance.getObservatoryUri(
appId,
device,
usesIpv6: usesIpv6,
deviceVmservicePort: deviceVmservicePort,
);
observatoryUri = Stream<Uri>
.fromFuture(
MDnsObservatoryDiscovery.instance.getObservatoryUri(
appId,
device,
usesIpv6: usesIpv6,
deviceVmservicePort: deviceVmservicePort,
)
).asBroadcastStream();
}
// If MDNS discovery fails or we're not on iOS, fallback to ProtocolDiscovery.
if (observatoryUri == null) {
ProtocolDiscovery observatoryDiscovery;
try {
observatoryDiscovery = ProtocolDiscovery.observatory(
final ProtocolDiscovery observatoryDiscovery =
ProtocolDiscovery.observatory(
device.getLogReader(),
portForwarder: device.portForwarder,
ipv6: ipv6,
devicePort: deviceVmservicePort,
hostPort: hostVmservicePort,
);
printStatus('Waiting for a connection from Flutter on ${device.name}...');
observatoryUri = await observatoryDiscovery.uri;
// Determine ipv6 status from the scanned logs.
usesIpv6 = observatoryDiscovery.ipv6;
printStatus('Done.'); // FYI, this message is used as a sentinel in tests.
} catch (error) {
throwToolExit('Failed to establish a debug connection with ${device.name}: $error');
} finally {
await observatoryDiscovery?.cancel();
}
printStatus('Waiting for a connection from Flutter on ${device.name}...');
observatoryUri = observatoryDiscovery.uris;
// Determine ipv6 status from the scanned logs.
usesIpv6 = observatoryDiscovery.ipv6;
}
} else {
observatoryUri = await buildObservatoryUri(
device,
debugUri?.host ?? hostname,
devicePort ?? debugUri.port,
hostVmservicePort,
debugUri?.path,
);
}
try {
final bool useHot = getBuildInfo().isDebug;
final FlutterDevice flutterDevice = await FlutterDevice.create(
device,
flutterProject: flutterProject,
trackWidgetCreation: boolArg('track-widget-creation'),
fileSystemRoots: stringsArg('filesystem-root'),
fileSystemScheme: stringArg('filesystem-scheme'),
viewFilter: stringArg('isolate-filter'),
target: stringArg('target'),
targetModel: TargetModel(stringArg('target-model')),
buildMode: getBuildMode(),
dartDefines: dartDefines,
);
flutterDevice.observatoryUris = <Uri>[ observatoryUri ];
final List<FlutterDevice> flutterDevices = <FlutterDevice>[flutterDevice];
final DebuggingOptions debuggingOptions = DebuggingOptions.enabled(getBuildInfo());
terminal.usesTerminalUi = daemon == null;
final ResidentRunner runner = useHot ?
hotRunnerFactory.build(
flutterDevices,
target: targetFile,
debuggingOptions: debuggingOptions,
packagesFilePath: globalResults['packages'] as String,
projectRootPath: stringArg('project-root'),
dillOutputPath: stringArg('output-dill'),
ipv6: usesIpv6,
flutterProject: flutterProject,
observatoryUri = Stream<Uri>
.fromFuture(
buildObservatoryUri(
device,
debugUri?.host ?? hostname,
devicePort ?? debugUri.port,
hostVmservicePort,
debugUri?.path,
)
: ColdRunner(
flutterDevices,
target: targetFile,
debuggingOptions: debuggingOptions,
ipv6: usesIpv6,
);
if (attachLogger) {
flutterDevice.startEchoingDeviceLog();
}
).asBroadcastStream();
}
terminal.usesTerminalUi = daemon == null;
try {
int result;
if (daemon != null) {
final ResidentRunner runner = await createResidentRunner(
observatoryUris: observatoryUri,
device: device,
flutterProject: flutterProject,
usesIpv6: usesIpv6,
);
AppInstance app;
try {
app = await daemon.appDomain.launch(
......@@ -324,20 +293,34 @@ class AttachCommand extends FlutterCommand {
}
result = await app.runner.waitForAppToFinish();
assert(result != null);
} else {
return;
}
while (true) {
final ResidentRunner runner = await createResidentRunner(
observatoryUris: observatoryUri,
device: device,
flutterProject: flutterProject,
usesIpv6: usesIpv6,
);
final Completer<void> onAppStart = Completer<void>.sync();
TerminalHandler terminalHandler;
unawaited(onAppStart.future.whenComplete(() {
TerminalHandler(runner)
terminalHandler = TerminalHandler(runner)
..setupTerminal()
..registerSignalHandlers();
}));
result = await runner.attach(
appStartedCompleter: onAppStart,
);
if (result != 0) {
throwToolExit(null, exitCode: result);
}
terminalHandler?.stop();
assert(result != null);
}
if (result != 0) {
throwToolExit(null, exitCode: result);
if (runner.exited || !runner.isWaitingForObservatory) {
break;
}
printStatus('Waiting for a new connection from Flutter on ${device.name}...');
}
} finally {
final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
......@@ -347,6 +330,52 @@ class AttachCommand extends FlutterCommand {
}
}
Future<ResidentRunner> createResidentRunner({
@required Stream<Uri> observatoryUris,
@required Device device,
@required FlutterProject flutterProject,
@required bool usesIpv6,
}) async {
assert(observatoryUris != null);
assert(device != null);
assert(flutterProject != null);
assert(usesIpv6 != null);
final FlutterDevice flutterDevice = await FlutterDevice.create(
device,
flutterProject: flutterProject,
trackWidgetCreation: boolArg('track-widget-creation'),
fileSystemRoots: stringsArg('filesystem-root'),
fileSystemScheme: stringArg('filesystem-scheme'),
viewFilter: stringArg('isolate-filter'),
target: stringArg('target'),
targetModel: TargetModel(stringArg('target-model')),
buildMode: getBuildMode(),
dartDefines: dartDefines,
);
flutterDevice.observatoryUris = observatoryUris;
final List<FlutterDevice> flutterDevices = <FlutterDevice>[flutterDevice];
final DebuggingOptions debuggingOptions = DebuggingOptions.enabled(getBuildInfo());
return getBuildInfo().isDebug
? hotRunnerFactory.build(
flutterDevices,
target: targetFile,
debuggingOptions: debuggingOptions,
packagesFilePath: globalResults['packages'] as String,
projectRootPath: stringArg('project-root'),
dillOutputPath: stringArg('output-dill'),
ipv6: usesIpv6,
flutterProject: flutterProject,
)
: ColdRunner(
flutterDevices,
target: targetFile,
debuggingOptions: debuggingOptions,
ipv6: usesIpv6,
);
}
Future<void> _validateArguments() async { }
}
......
......@@ -11,6 +11,7 @@ import 'asset.dart';
import 'base/context.dart';
import 'base/file_system.dart';
import 'base/io.dart';
import 'base/net.dart';
import 'build_info.dart';
import 'bundle.dart';
import 'compile.dart';
......@@ -265,17 +266,20 @@ class DevFSException implements Exception {
class _DevFSHttpWriter {
_DevFSHttpWriter(this.fsName, VMService serviceProtocol)
: httpAddress = serviceProtocol.httpAddress;
: httpAddress = serviceProtocol.httpAddress,
_client = (context.get<HttpClientFactory>() == null)
? HttpClient()
: context.get<HttpClientFactory>()();
final String fsName;
final Uri httpAddress;
final HttpClient _client;
static const int kMaxInFlight = 6;
int _inFlight = 0;
Map<Uri, DevFSContent> _outstanding;
Completer<void> _completer;
final HttpClient _client = HttpClient();
Future<void> write(Map<Uri, DevFSContent> entries) async {
_client.maxConnectionsPerHost = kMaxInFlight;
......
......@@ -17,16 +17,23 @@ class ProtocolDiscovery {
this.logReader,
this.serviceName, {
this.portForwarder,
this.throttleDuration,
this.hostPort,
this.devicePort,
this.ipv6,
}) : assert(logReader != null) {
_deviceLogSubscription = logReader.logLines.listen(_handleLine);
}) : assert(logReader != null)
{
_deviceLogSubscription = logReader.logLines.listen(
_handleLine,
onDone: _stopScrapingLogs,
);
_uriStreamController = _BufferedStreamController<Uri>();
}
factory ProtocolDiscovery.observatory(
DeviceLogReader logReader, {
DevicePortForwarder portForwarder,
Duration throttleDuration = const Duration(milliseconds: 200),
@required int hostPort,
@required int devicePort,
@required bool ipv6,
......@@ -36,6 +43,7 @@ class ProtocolDiscovery {
logReader,
kObservatoryService,
portForwarder: portForwarder,
throttleDuration: throttleDuration,
hostPort: hostPort,
devicePort: devicePort,
ipv6: ipv6,
......@@ -49,50 +57,70 @@ class ProtocolDiscovery {
final int devicePort;
final bool ipv6;
final Completer<Uri> _completer = Completer<Uri>();
/// The time to wait before forwarding a new observatory URIs from [logReader].
final Duration throttleDuration;
StreamSubscription<String> _deviceLogSubscription;
_BufferedStreamController<Uri> _uriStreamController;
/// The discovered service URI.
/// Use [uris] instead.
// TODO(egarciad): replace `uri` for `uris`.
Future<Uri> get uri {
return uris.first;
}
/// The discovered service URIs.
///
/// Port forwarding is only attempted when this is invoked, in case we never
/// need to port forward.
Future<Uri> get uri async {
final Uri rawUri = await _completer.future;
return await _forwardPort(rawUri);
/// When a new observatory URI is available in [logReader],
/// the URIs are forwarded at most once every [throttleDuration].
///
/// Port forwarding is only attempted when this is invoked,
/// for each observatory URI in the stream.
Stream<Uri> get uris {
return _uriStreamController.stream
.transform(_throttle<Uri>(
waitDuration: throttleDuration,
))
.asyncMap<Uri>(_forwardPort);
}
Future<void> cancel() => _stopScrapingLogs();
Future<void> _stopScrapingLogs() async {
await _uriStreamController?.close();
await _deviceLogSubscription?.cancel();
_deviceLogSubscription = null;
}
void _handleLine(String line) {
Uri uri;
Match _getPatternMatch(String line) {
final RegExp r = RegExp('${RegExp.escape(serviceName)} listening on ((http|\/\/)[a-zA-Z0-9:/=_\\-\.\\[\\]]+)');
final Match match = r.firstMatch(line);
return r.firstMatch(line);
}
Uri _getObservatoryUri(String line) {
final Match match = _getPatternMatch(line);
if (match != null) {
try {
uri = Uri.parse(match[1]);
} on FormatException catch (error, stackTrace) {
_stopScrapingLogs();
_completer.completeError(error, stackTrace);
}
return Uri.parse(match[1]);
}
return null;
}
void _handleLine(String line) {
Uri uri;
try {
uri = _getObservatoryUri(line);
} on FormatException catch(error, stackTrace) {
_uriStreamController.addError(error, stackTrace);
}
if (uri == null) {
return;
}
if (devicePort != null && uri.port != devicePort) {
if (devicePort != null && uri.port != devicePort) {
printTrace('skipping potential observatory $uri due to device port mismatch');
return;
}
assert(!_completer.isCompleted);
_stopScrapingLogs();
_completer.complete(uri);
_uriStreamController.add(uri);
}
Future<Uri> _forwardPort(Uri deviceUri) async {
......@@ -110,7 +138,101 @@ class ProtocolDiscovery {
if (ipv6) {
hostUri = hostUri.replace(host: InternetAddress.loopbackIPv6.host);
}
return hostUri;
}
}
/// Provides a broadcast stream controller that buffers the events
/// if there isn't a listener attached.
/// The events are then delivered when a listener is attached to the stream.
class _BufferedStreamController<T> {
_BufferedStreamController() : _events = <dynamic>[];
/// The stream that this controller is controlling.
Stream<T> get stream {
return _streamController.stream;
}
StreamController<T> _streamControllerInstance;
StreamController<T> get _streamController {
_streamControllerInstance ??= StreamController<T>.broadcast(onListen: () {
for (dynamic event in _events) {
assert(!(T is List));
if (event is T) {
_streamControllerInstance.add(event);
} else {
_streamControllerInstance.addError(
event.first as Object,
event.last as StackTrace,
);
}
}
_events.clear();
});
return _streamControllerInstance;
}
final List<dynamic> _events;
/// Sends [event] if there is a listener attached to the broadcast stream.
/// Otherwise, it enqueues [event] until a listener is attached.
void add(T event) {
if (_streamController.hasListener) {
_streamController.add(event);
} else {
_events.add(event);
}
}
/// Sends or enqueues an error event.
void addError(Object error, [StackTrace stackTrace]) {
if (_streamController.hasListener) {
_streamController.addError(error, stackTrace);
} else {
_events.add(<dynamic>[error, stackTrace]);
}
}
/// Closes the stream.
Future<void> close() {
return _streamController.close();
}
}
/// This transformer will produce an event at most once every [waitDuration].
///
/// 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.
StreamTransformer<S, S> _throttle<S>({
@required Duration waitDuration,
}) {
assert(waitDuration != null);
S latestLine;
int lastExecution;
Future<void> throttleFuture;
return StreamTransformer<S, S>
.fromHandlers(
handleData: (S value, EventSink<S> sink) {
latestLine = value;
final int currentTime = DateTime.now().millisecondsSinceEpoch;
lastExecution ??= currentTime;
final int remainingTime = currentTime - lastExecution;
final int nextExecutionTime = remainingTime > waitDuration.inMilliseconds
? 0
: waitDuration.inMilliseconds - remainingTime;
throttleFuture ??= Future<void>
.delayed(Duration(milliseconds: nextExecutionTime))
.whenComplete(() {
sink.add(latestLine);
throttleFuture = null;
lastExecution = DateTime.now().millisecondsSinceEpoch;
});
}
);
}
......@@ -131,16 +131,20 @@ class FlutterDevice {
final Device device;
final ResidentCompiler generator;
List<Uri> observatoryUris;
Stream<Uri> observatoryUris;
List<VMService> vmServices;
DevFS devFS;
ApplicationPackage package;
List<String> fileSystemRoots;
String fileSystemScheme;
StreamSubscription<String> _loggingSubscription;
bool _isListeningForObservatoryUri;
final String viewFilter;
final bool trackWidgetCreation;
/// Whether the stream [observatoryUris] is still open.
bool get isWaitingForObservatory => _isListeningForObservatoryUri ?? false;
/// If the [reloadSources] parameter is not null the 'reloadSources' service
/// will be registered.
/// The 'reloadSources' service can be used by other Service Protocol clients
......@@ -154,23 +158,50 @@ class FlutterDevice {
ReloadSources reloadSources,
Restart restart,
CompileExpression compileExpression,
}) async {
if (vmServices != null) {
return;
}
final List<VMService> localVmServices = List<VMService>(observatoryUris.length);
for (int i = 0; i < observatoryUris.length; i += 1) {
printTrace('Connecting to service protocol: ${observatoryUris[i]}');
localVmServices[i] = await VMService.connect(
observatoryUris[i],
reloadSources: reloadSources,
restart: restart,
compileExpression: compileExpression,
);
printTrace('Successfully connected to service protocol: ${observatoryUris[i]}');
}
vmServices = localVmServices;
device.getLogReader(app: package).connectedVMServices = vmServices;
}) {
final Completer<void> completer = Completer<void>();
StreamSubscription<void> subscription;
bool isWaitingForVm = false;
subscription = observatoryUris.listen((Uri observatoryUri) async {
// FYI, this message is used as a sentinel in tests.
printTrace('Connecting to service protocol: $observatoryUri');
isWaitingForVm = true;
VMService service;
try {
service = await VMService.connect(
observatoryUri,
reloadSources: reloadSources,
restart: restart,
compileExpression: compileExpression,
);
} on Exception catch (exception) {
printTrace('Fail to connect to service protocol: $observatoryUri: $exception');
if (!completer.isCompleted && !_isListeningForObservatoryUri) {
completer.completeError('failed to connect to $observatoryUri');
}
return;
}
if (completer.isCompleted) {
return;
}
printTrace('Successfully connected to service protocol: $observatoryUri');
vmServices = <VMService>[service];
device.getLogReader(app: package).connectedVMServices = vmServices;
completer.complete();
await subscription.cancel();
}, onError: (dynamic error) {
printTrace('Fail to handle observatory URI: $error');
}, onDone: () {
_isListeningForObservatoryUri = false;
if (!completer.isCompleted && !isWaitingForVm) {
completer.completeError('connection to device ended too early');
}
});
_isListeningForObservatoryUri = true;
return completer.future;
}
Future<void> refreshViews() async {
......@@ -221,6 +252,7 @@ class FlutterDevice {
if (flutterViews.any((FlutterView view) {
return view != null &&
view.uiIsolate != null &&
view.uiIsolate.pauseEvent != null &&
view.uiIsolate.pauseEvent.isPauseEvent;
}
)) {
......@@ -431,9 +463,13 @@ class FlutterDevice {
return 2;
}
if (result.hasObservatory) {
observatoryUris = <Uri>[result.observatoryUri];
observatoryUris = Stream<Uri>
.value(result.observatoryUri)
.asBroadcastStream();
} else {
observatoryUris = <Uri>[];
observatoryUris = const Stream<Uri>
.empty()
.asBroadcastStream();
}
return 0;
}
......@@ -491,9 +527,13 @@ class FlutterDevice {
return 2;
}
if (result.hasObservatory) {
observatoryUris = <Uri>[result.observatoryUri];
observatoryUris = Stream<Uri>
.value(result.observatoryUri)
.asBroadcastStream();
} else {
observatoryUris = <Uri>[];
observatoryUris = const Stream<Uri>
.empty()
.asBroadcastStream();
}
return 0;
}
......@@ -613,14 +653,21 @@ abstract class ResidentRunner {
/// The parent location of the incremental artifacts.
@visibleForTesting
final Directory artifactDirectory;
final Completer<int> _finished = Completer<int>();
final String packagesFilePath;
final String projectRootPath;
final String mainPath;
final AssetBundle assetBundle;
bool _exited = false;
bool hotMode ;
Completer<int> _finished = Completer<int>();
bool hotMode;
/// Returns true if every device is streaming observatory URIs.
bool get isWaitingForObservatory {
return flutterDevices.every((FlutterDevice device) {
return device.isWaitingForObservatory;
});
}
String get dillOutputPath => _dillOutputPath ?? fs.path.join(artifactDirectory.path, 'app.dill');
String getReloadPath({ bool fullRestart }) => mainPath + (fullRestart ? '' : '.incremental') + '.dill';
......@@ -631,6 +678,9 @@ abstract class ResidentRunner {
bool get isRunningRelease => debuggingOptions.buildInfo.isRelease;
bool get supportsServiceProtocol => isRunningDebug || isRunningProfile;
/// Returns [true] if the resident runner exited after invoking [exit()].
bool get exited => _exited;
/// Whether this runner can hot restart.
///
/// To prevent scenarios where only a subset of devices are hot restarted,
......@@ -862,6 +912,8 @@ abstract class ResidentRunner {
throw 'The service protocol is not enabled.';
}
_finished = Completer<int>();
bool viewFound = false;
for (FlutterDevice device in flutterDevices) {
await device.connect(
......@@ -1045,15 +1097,33 @@ class TerminalHandler {
subscription = terminal.keystrokes.listen(processTerminalInput);
}
final Map<io.ProcessSignal, Object> _signalTokens = <io.ProcessSignal, Object>{};
void _addSignalHandler(io.ProcessSignal signal, SignalHandler handler) {
_signalTokens[signal] = signals.addHandler(signal, handler);
}
void registerSignalHandlers() {
assert(residentRunner.stayResident);
signals.addHandler(io.ProcessSignal.SIGINT, _cleanUp);
signals.addHandler(io.ProcessSignal.SIGTERM, _cleanUp);
_addSignalHandler(io.ProcessSignal.SIGINT, _cleanUp);
_addSignalHandler(io.ProcessSignal.SIGTERM, _cleanUp);
if (!residentRunner.supportsServiceProtocol || !residentRunner.supportsRestart) {
return;
}
signals.addHandler(io.ProcessSignal.SIGUSR1, _handleSignal);
signals.addHandler(io.ProcessSignal.SIGUSR2, _handleSignal);
_addSignalHandler(io.ProcessSignal.SIGUSR1, _handleSignal);
_addSignalHandler(io.ProcessSignal.SIGUSR2, _handleSignal);
}
/// Unregisters terminal signal and keystroke handlers.
void stop() {
assert(residentRunner.stayResident);
for (MapEntry<io.ProcessSignal, Object> entry in _signalTokens.entries) {
signals.removeHandler(entry.key, entry.value);
}
_signalTokens.clear();
subscription.cancel();
}
/// Returns [true] if the input has been handled by this function.
......
......@@ -83,7 +83,7 @@ class ColdRunner extends ResidentRunner {
if (flutterDevices.first.observatoryUris != null) {
// For now, only support one debugger connection.
connectionInfoCompleter?.complete(DebugConnectionInfo(
httpUri: flutterDevices.first.observatoryUris.first,
httpUri: flutterDevices.first.vmServices.first.httpAddress,
wsUri: flutterDevices.first.vmServices.first.wsAddress,
));
}
......@@ -183,10 +183,9 @@ class ColdRunner extends ResidentRunner {
bool haveAnything = false;
for (FlutterDevice device in flutterDevices) {
final String dname = device.device.name;
if (device.observatoryUris != null) {
for (Uri uri in device.observatoryUris) {
printStatus('An Observatory debugger and profiler on $dname is available at $uri');
haveAnything = true;
if (device.vmServices != null) {
for (VMService vm in device.vmServices) {
printStatus('An Observatory debugger and profiler on $dname is available at: ${vm.httpAddress}');
}
}
}
......
......@@ -180,7 +180,7 @@ class HotRunner extends ResidentRunner {
// Only handle one debugger connection.
connectionInfoCompleter.complete(
DebugConnectionInfo(
httpUri: flutterDevices.first.observatoryUris.first,
httpUri: flutterDevices.first.vmServices.first.httpAddress,
wsUri: flutterDevices.first.vmServices.first.wsAddress,
baseUri: baseUris.first.toString(),
),
......@@ -987,8 +987,8 @@ class HotRunner extends ResidentRunner {
printStatus(message);
for (FlutterDevice device in flutterDevices) {
final String dname = device.device.name;
for (Uri uri in device.observatoryUris) {
printStatus('An Observatory debugger and profiler on $dname is available at: $uri');
for (VMService vm in device.vmServices) {
printStatus('An Observatory debugger and profiler on $dname is available at: ${vm.httpAddress}');
}
}
final String quitMessage = _didAttach
......
......@@ -10,6 +10,7 @@ import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/net.dart';
import 'package:flutter_tools/src/compile.dart';
import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/vmservice.dart';
......@@ -159,6 +160,7 @@ void main() {
verify(httpRequest.close()).called(kFailedAttempts + 1);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
HttpClientFactory: () => () => httpClient,
ProcessManager: () => FakeProcessManager.any(),
});
});
......@@ -208,6 +210,7 @@ void main() {
expect(report.success, true);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
HttpClient: () => () => HttpClient(),
ProcessManager: () => FakeProcessManager.any(),
});
......@@ -310,6 +313,7 @@ void main() {
expect(devFS.lastCompiled, isNot(previousCompile));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
HttpClient: () => () => HttpClient(),
ProcessManager: () => FakeProcessManager.any(),
});
});
......
......@@ -95,9 +95,7 @@ void main() {
when(mockFlutterView.uiIsolate).thenReturn(mockIsolate);
when(mockFlutterView.runFromSource(any, any, any)).thenAnswer((Invocation invocation) async {});
when(mockFlutterDevice.stopEchoingDeviceLog()).thenAnswer((Invocation invocation) async { });
when(mockFlutterDevice.observatoryUris).thenReturn(<Uri>[
testUri,
]);
when(mockFlutterDevice.observatoryUris).thenAnswer((_) => Stream<Uri>.value(testUri));
when(mockFlutterDevice.connect(
reloadSources: anyNamed('reloadSources'),
restart: anyNamed('restart'),
......@@ -636,7 +634,7 @@ void main() {
final TestFlutterDevice flutterDevice = TestFlutterDevice(
mockDevice,
<FlutterView>[],
observatoryUris: <Uri>[ testUri ]
observatoryUris: Stream<Uri>.value(testUri),
);
await flutterDevice.connect();
......@@ -657,7 +655,7 @@ class MockUsage extends Mock implements Usage {}
class MockProcessManager extends Mock implements ProcessManager {}
class MockServiceEvent extends Mock implements ServiceEvent {}
class TestFlutterDevice extends FlutterDevice {
TestFlutterDevice(Device device, this.views, { List<Uri> observatoryUris })
TestFlutterDevice(Device device, this.views, { Stream<Uri> observatoryUris })
: super(device, buildMode: BuildMode.debug, trackWidgetCreation: false) {
_observatoryUris = observatoryUris;
}
......@@ -666,8 +664,8 @@ class TestFlutterDevice extends FlutterDevice {
final List<FlutterView> views;
@override
List<Uri> get observatoryUris => _observatoryUris;
List<Uri> _observatoryUris;
Stream<Uri> get observatoryUris => _observatoryUris;
Stream<Uri> _observatoryUris;
}
class ThrowingForwardingFileSystem extends ForwardingFileSystem {
......
......@@ -533,6 +533,12 @@ class MockAndroidDevice extends Mock implements AndroidDevice {
@override
bool isSupported() => true;
@override
bool get supportsHotRestart => true;
@override
bool get supportsFlutterExit => false;
@override
bool isSupportedForProject(FlutterProject flutterProject) => true;
}
......@@ -563,16 +569,33 @@ class MockDeviceLogReader extends DeviceLogReader {
@override
String get name => 'MockLogReader';
final StreamController<String> _linesController = StreamController<String>.broadcast();
StreamController<String> _cachedLinesController;
final List<String> _lineQueue = <String>[];
StreamController<String> get _linesController {
_cachedLinesController ??= StreamController<String>
.broadcast(onListen: () {
_lineQueue.forEach(_linesController.add);
_lineQueue.clear();
});
return _cachedLinesController;
}
@override
Stream<String> get logLines => _linesController.stream;
void addLine(String line) => _linesController.add(line);
void addLine(String line) {
if (_linesController.hasListener) {
_linesController.add(line);
} else {
_lineQueue.add(line);
}
}
@override
void dispose() {
_linesController.close();
Future<void> dispose() async {
_lineQueue.clear();
await _linesController.close();
}
}
......
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