Unverified Commit 37787982 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] prevent StateError when log reader is disposed (#52173)

Previously the AdbLogReader did async setup in the StreamController.onListen callback, specifically it would query the api version and start the adb process. If the log subscription was cancelled before this setup completed, then the log output could (haven't confirmed) get added to a closed controller, causing the above state error. 
parent fdd023fa
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'dart:async'; import 'dart:async';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:process/process.dart';
import '../android/android_builder.dart'; import '../android/android_builder.dart';
import '../android/android_sdk.dart'; import '../android/android_sdk.dart';
...@@ -219,13 +220,14 @@ class AndroidDevice extends Device { ...@@ -219,13 +220,14 @@ class AndroidDevice extends Device {
@override @override
Future<String> get sdkNameAndVersion async => Future<String> get sdkNameAndVersion async =>
'Android ${await _sdkVersion} (API ${await _apiVersion})'; 'Android ${await _sdkVersion} (API ${await apiVersion})';
Future<String> get _sdkVersion => _getProperty('ro.build.version.release'); Future<String> get _sdkVersion => _getProperty('ro.build.version.release');
Future<String> get _apiVersion => _getProperty('ro.build.version.sdk'); @visibleForTesting
Future<String> get apiVersion => _getProperty('ro.build.version.sdk');
_AdbLogReader _logReader; AdbLogReader _logReader;
_AndroidDevicePortForwarder _portForwarder; _AndroidDevicePortForwarder _portForwarder;
List<String> adbCommandForDevice(List<String> args) { List<String> adbCommandForDevice(List<String> args) {
...@@ -554,7 +556,7 @@ class AndroidDevice extends Device { ...@@ -554,7 +556,7 @@ class AndroidDevice extends Device {
// TODO(devoncarew): Remember the forwarding information (so we can later remove the // TODO(devoncarew): Remember the forwarding information (so we can later remove the
// port forwarding or set it up again when adb fails on us). // port forwarding or set it up again when adb fails on us).
observatoryDiscovery = ProtocolDiscovery.observatory( observatoryDiscovery = ProtocolDiscovery.observatory(
getLogReader(), await getLogReader(),
portForwarder: portForwarder, portForwarder: portForwarder,
hostPort: debuggingOptions.hostVmServicePort, hostPort: debuggingOptions.hostVmServicePort,
devicePort: debuggingOptions.deviceVmServicePort, devicePort: debuggingOptions.deviceVmServicePort,
...@@ -677,10 +679,9 @@ class AndroidDevice extends Device { ...@@ -677,10 +679,9 @@ class AndroidDevice extends Device {
} }
@override @override
DeviceLogReader getLogReader({ AndroidApk app }) { FutureOr<DeviceLogReader> getLogReader({ AndroidApk app }) async {
// The Android log reader isn't app-specific. // The Android log reader isn't app-specific.
_logReader ??= _AdbLogReader(this); return _logReader ??= await AdbLogReader.createLogReader(this, globals.processManager);
return _logReader;
} }
@override @override
...@@ -1013,55 +1014,66 @@ void parseADBDeviceOutput( ...@@ -1013,55 +1014,66 @@ void parseADBDeviceOutput(
} }
/// A log reader that logs from `adb logcat`. /// A log reader that logs from `adb logcat`.
class _AdbLogReader extends DeviceLogReader { class AdbLogReader extends DeviceLogReader {
_AdbLogReader(this.device) { AdbLogReader._(this._adbProcess, this.name) {
_linesController = StreamController<String>.broadcast( _linesController = StreamController<String>.broadcast(
onListen: _start, onListen: _start,
onCancel: _stop, onCancel: _stop,
); );
} }
final AndroidDevice device; @visibleForTesting
factory AdbLogReader.test(Process adbProcess, String name) = AdbLogReader._;
StreamController<String> _linesController;
Process _process;
@override /// Create a new [AdbLogReader] from an [AndroidDevice] instance.
Stream<String> get logLines => _linesController.stream; static Future<AdbLogReader> createLogReader(
AndroidDevice device,
@override ProcessManager processManager,
String get name => device.name; ) async {
Future<void> _start() async {
final String lastTimestamp = device.lastLogcatTimestamp;
// Start the adb logcat process and filter the most recent logs since `lastTimestamp`.
final List<String> args = <String>[
'logcat',
'-v',
'time',
];
// logcat -T is not supported on Android releases before Lollipop. // logcat -T is not supported on Android releases before Lollipop.
const int kLollipopVersionCode = 21; const int kLollipopVersionCode = 21;
final int apiVersion = (String v) { final int apiVersion = (String v) {
// If the API version string isn't found, conservatively assume that the // If the API version string isn't found, conservatively assume that the
// version is less recent than the one we're looking for. // version is less recent than the one we're looking for.
return v == null ? kLollipopVersionCode - 1 : int.tryParse(v); return v == null ? kLollipopVersionCode - 1 : int.tryParse(v);
}(await device._apiVersion); }(await device.apiVersion);
if (apiVersion != null && apiVersion >= kLollipopVersionCode) {
args.addAll(<String>[ // Start the adb logcat process and filter the most recent logs since `lastTimestamp`.
final List<String> args = <String>[
'logcat',
'-v',
'time',
if (apiVersion != null && apiVersion >= kLollipopVersionCode) ...<String>[
// Empty `-T` means the timestamp of the logcat command invocation.
'-T', '-T',
lastTimestamp ?? '', // Empty `-T` means the timestamp of the logcat command invocation. device.lastLogcatTimestamp ?? '',
]); ],
];
final Process process = await processManager.start(device.adbCommandForDevice(args));
return AdbLogReader._(process, device.name);
} }
_process = await processUtils.start(device.adbCommandForDevice(args)); final Process _adbProcess;
@override
final String name;
StreamController<String> _linesController;
@override
Stream<String> get logLines => _linesController.stream;
void _start() {
// We expect logcat streams to occasionally contain invalid utf-8, // We expect logcat streams to occasionally contain invalid utf-8,
// see: https://github.com/flutter/flutter/pull/8864. // see: https://github.com/flutter/flutter/pull/8864.
const Utf8Decoder decoder = Utf8Decoder(reportErrors: false); const Utf8Decoder decoder = Utf8Decoder(reportErrors: false);
_process.stdout.transform<String>(decoder).transform<String>(const LineSplitter()).listen(_onLine); _adbProcess.stdout.transform<String>(decoder)
_process.stderr.transform<String>(decoder).transform<String>(const LineSplitter()).listen(_onLine); .transform<String>(const LineSplitter())
unawaited(_process.exitCode.whenComplete(() { .listen(_onLine);
_adbProcess.stderr.transform<String>(decoder)
.transform<String>(const LineSplitter())
.listen(_onLine);
unawaited(_adbProcess.exitCode.whenComplete(() {
if (_linesController.hasListener) { if (_linesController.hasListener) {
_linesController.close(); _linesController.close();
} }
...@@ -1101,6 +1113,11 @@ class _AdbLogReader extends DeviceLogReader { ...@@ -1101,6 +1113,11 @@ class _AdbLogReader extends DeviceLogReader {
// adb logcat. We are currently passing 'time', which has the format: // adb logcat. We are currently passing 'time', which has the format:
// mm-dd hh:mm:ss.milliseconds Priority/Tag( PID): .... // mm-dd hh:mm:ss.milliseconds Priority/Tag( PID): ....
void _onLine(String line) { void _onLine(String line) {
// This line might be processed after the subscription is closed but before
// adb stops streaming logs.
if (_linesController.isClosed) {
return;
}
final Match timeMatch = AndroidDevice._timeRegExp.firstMatch(line); final Match timeMatch = AndroidDevice._timeRegExp.firstMatch(line);
if (timeMatch == null || line.length == timeMatch.end) { if (timeMatch == null || line.length == timeMatch.end) {
return; return;
...@@ -1160,7 +1177,8 @@ class _AdbLogReader extends DeviceLogReader { ...@@ -1160,7 +1177,8 @@ class _AdbLogReader extends DeviceLogReader {
} }
void _stop() { void _stop() {
_process?.kill(); _linesController.close();
_adbProcess?.kill();
} }
@override @override
......
...@@ -245,7 +245,7 @@ class AttachCommand extends FlutterCommand { ...@@ -245,7 +245,7 @@ class AttachCommand extends FlutterCommand {
if (observatoryUri == null) { if (observatoryUri == null) {
final ProtocolDiscovery observatoryDiscovery = final ProtocolDiscovery observatoryDiscovery =
ProtocolDiscovery.observatory( ProtocolDiscovery.observatory(
device.getLogReader(), await device.getLogReader(),
portForwarder: device.portForwarder, portForwarder: device.portForwarder,
ipv6: ipv6, ipv6: ipv6,
devicePort: deviceVmservicePort, devicePort: deviceVmservicePort,
......
...@@ -427,9 +427,8 @@ Future<LaunchResult> _startApp(DriveCommand command, Uri webUri) async { ...@@ -427,9 +427,8 @@ Future<LaunchResult> _startApp(DriveCommand command, Uri webUri) async {
globals.printTrace('Starting application.'); globals.printTrace('Starting application.');
// Forward device log messages to the terminal window running the "drive" command. // Forward device log messages to the terminal window running the "drive" command.
command._deviceLogSubscription = command final DeviceLogReader logReader = await command.device.getLogReader(app: package);
.device command._deviceLogSubscription = logReader
.getLogReader(app: package)
.logLines .logLines
.listen(globals.printStatus); .listen(globals.printStatus);
......
...@@ -46,7 +46,7 @@ class LogsCommand extends FlutterCommand { ...@@ -46,7 +46,7 @@ class LogsCommand extends FlutterCommand {
device.clearLogs(); device.clearLogs();
} }
final DeviceLogReader logReader = device.getLogReader(); final DeviceLogReader logReader = await device.getLogReader();
Cache.releaseLockEarly(); Cache.releaseLockEarly();
......
...@@ -383,7 +383,7 @@ abstract class Device { ...@@ -383,7 +383,7 @@ abstract class Device {
/// Get a log reader for this device. /// Get a log reader for this device.
/// If [app] is specified, this will return a log reader specific to that /// If [app] is specified, this will return a log reader specific to that
/// application. Otherwise, a global log reader will be returned. /// application. Otherwise, a global log reader will be returned.
DeviceLogReader getLogReader({ covariant ApplicationPackage app }); FutureOr<DeviceLogReader> getLogReader({ covariant ApplicationPackage app });
/// Get the port forwarder for this device. /// Get the port forwarder for this device.
DevicePortForwarder get portForwarder; DevicePortForwarder get portForwarder;
......
...@@ -193,7 +193,7 @@ class FlutterDevice { ...@@ -193,7 +193,7 @@ class FlutterDevice {
globals.printTrace('Successfully connected to service protocol: $observatoryUri'); globals.printTrace('Successfully connected to service protocol: $observatoryUri');
vmService = service; vmService = service;
device.getLogReader(app: package).connectedVMService = vmService; (await device.getLogReader(app: package)).connectedVMService = vmService;
completer.complete(); completer.complete();
await subscription.cancel(); await subscription.cancel();
}, onError: (dynamic error) { }, onError: (dynamic error) {
...@@ -369,11 +369,11 @@ class FlutterDevice { ...@@ -369,11 +369,11 @@ class FlutterDevice {
return to; return to;
} }
void startEchoingDeviceLog() { Future<void> startEchoingDeviceLog() async {
if (_loggingSubscription != null) { if (_loggingSubscription != null) {
return; return;
} }
final Stream<String> logStream = device.getLogReader(app: package).logLines; final Stream<String> logStream = (await device.getLogReader(app: package)).logLines;
if (logStream == null) { if (logStream == null) {
globals.printError('Failed to read device log stream'); globals.printError('Failed to read device log stream');
return; return;
...@@ -393,8 +393,8 @@ class FlutterDevice { ...@@ -393,8 +393,8 @@ class FlutterDevice {
_loggingSubscription = null; _loggingSubscription = null;
} }
void initLogReader() { Future<void> initLogReader() async {
device.getLogReader(app: package).appPid = vmService.vm.pid; (await device.getLogReader(app: package)).appPid = vmService.vm.pid;
} }
Future<int> runHot({ Future<int> runHot({
...@@ -426,7 +426,7 @@ class FlutterDevice { ...@@ -426,7 +426,7 @@ class FlutterDevice {
final Map<String, dynamic> platformArgs = <String, dynamic>{}; final Map<String, dynamic> platformArgs = <String, dynamic>{};
startEchoingDeviceLog(); await startEchoingDeviceLog();
// Start the application. // Start the application.
final Future<LaunchResult> futureResult = device.startApp( final Future<LaunchResult> futureResult = device.startApp(
...@@ -499,7 +499,7 @@ class FlutterDevice { ...@@ -499,7 +499,7 @@ class FlutterDevice {
platformArgs['trace-startup'] = coldRunner.traceStartup; platformArgs['trace-startup'] = coldRunner.traceStartup;
} }
startEchoingDeviceLog(); await startEchoingDeviceLog();
final LaunchResult result = await device.startApp( final LaunchResult result = await device.startApp(
package, package,
......
...@@ -94,7 +94,7 @@ class ColdRunner extends ResidentRunner { ...@@ -94,7 +94,7 @@ class ColdRunner extends ResidentRunner {
if (device.vmService == null) { if (device.vmService == null) {
continue; continue;
} }
device.initLogReader(); await device.initLogReader();
await device.refreshViews(); await device.refreshViews();
globals.printTrace('Connected to ${device.device.name}'); globals.printTrace('Connected to ${device.device.name}');
} }
...@@ -143,7 +143,7 @@ class ColdRunner extends ResidentRunner { ...@@ -143,7 +143,7 @@ class ColdRunner extends ResidentRunner {
return 2; return 2;
} }
for (final FlutterDevice device in flutterDevices) { for (final FlutterDevice device in flutterDevices) {
device.initLogReader(); await device.initLogReader();
} }
await refreshViews(); await refreshViews();
for (final FlutterDevice device in flutterDevices) { for (final FlutterDevice device in flutterDevices) {
......
...@@ -239,7 +239,7 @@ class HotRunner extends ResidentRunner { ...@@ -239,7 +239,7 @@ class HotRunner extends ResidentRunner {
} }
for (final FlutterDevice device in flutterDevices) { for (final FlutterDevice device in flutterDevices) {
device.initLogReader(); await device.initLogReader();
} }
try { try {
final List<Uri> baseUris = await _initDevFS(); final List<Uri> baseUris = await _initDevFS();
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// 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:flutter_tools/src/android/android_device.dart';
import 'package:mockito/mockito.dart';
import '../../src/common.dart';
import '../../src/context.dart';
const int kLollipopVersionCode = 21;
const String kLastLogcatTimestamp = '11-27 15:39:04.506';
void main() {
testWithoutContext('AdbLogReader calls adb logcat with expected flags apiVersion 21', () async {
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: <String>[
'adb',
'-s',
'1234',
'logcat',
'-v',
'time',
'-T',
kLastLogcatTimestamp,
],
)
]);
await AdbLogReader.createLogReader(
createMockDevice(kLollipopVersionCode),
processManager,
);
expect(processManager.hasRemainingExpectations, false);
});
testWithoutContext('AdbLogReader calls adb logcat with expected flags apiVersion < 21', () async {
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: <String>[
'adb',
'-s',
'1234',
'logcat',
'-v',
'time',
],
)
]);
await AdbLogReader.createLogReader(
createMockDevice(kLollipopVersionCode - 1),
processManager,
);
expect(processManager.hasRemainingExpectations, false);
});
testWithoutContext('AdbLogReader calls adb logcat with expected flags null apiVersion', () async {
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: <String>[
'adb',
'-s',
'1234',
'logcat',
'-v',
'time',
],
)
]);
await AdbLogReader.createLogReader(
createMockDevice(null),
processManager,
);
expect(processManager.hasRemainingExpectations, false);
});
testWithoutContext('AdbLogReader handles process early exit', () async {
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
FakeCommand(
command: const <String>[
'adb',
'-s',
'1234',
'logcat',
'-v',
'time',
],
completer: Completer<void>.sync(),
stdout: 'Hello There\n',
)
]);
final AdbLogReader logReader = await AdbLogReader.createLogReader(
createMockDevice(null),
processManager,
);
final Completer<void> onDone = Completer<void>.sync();
logReader.logLines.listen((String _) { }, onDone: onDone.complete);
logReader.dispose();
await onDone.future;
});
}
MockAndroidDevice createMockDevice(int sdkLevel) {
final MockAndroidDevice mockAndroidDevice = MockAndroidDevice();
when(mockAndroidDevice.apiVersion)
.thenAnswer((Invocation invocation) async => sdkLevel.toString());
when(mockAndroidDevice.lastLogcatTimestamp).thenReturn(kLastLogcatTimestamp);
when(mockAndroidDevice.adbCommandForDevice(any))
.thenAnswer((Invocation invocation) => <String>[
'adb', '-s', '1234', ...invocation.positionalArguments.first as List<String>
]);
return mockAndroidDevice;
}
class MockAndroidDevice extends Mock implements AndroidDevice {}
...@@ -693,97 +693,6 @@ flutter: ...@@ -693,97 +693,6 @@ flutter:
}); });
}); });
group('logReader', () {
ProcessManager mockProcessManager;
AndroidSdk mockAndroidSdk;
setUp(() {
mockAndroidSdk = MockAndroidSdk();
mockProcessManager = MockProcessManager();
});
void setupGetprop({int apiVersion}) {
when(mockProcessManager.run(
argThat(contains('getprop')),
stderrEncoding: anyNamed('stderrEncoding'),
stdoutEncoding: anyNamed('stdoutEncoding'),
)).thenAnswer((_) {
final String buf = apiVersion == null
? ''
: '[ro.build.version.sdk]: [$apiVersion]\n';
final ProcessResult result = ProcessResult(1, 0, buf, '');
return Future<ProcessResult>.value(result);
});
}
const int kLollipopVersionCode = 21;
void callsAdbLogcatCorrectly({int apiVersion}) {
testUsingContext('calls adb logcat with expected flags, apiVersion=$apiVersion', () async {
const String kLastLogcatTimestamp = '11-27 15:39:04.506';
setupGetprop(apiVersion: apiVersion);
when(mockAndroidSdk.adbPath).thenReturn('adb');
when(mockProcessManager.runSync(<String>[
'adb', '-s', '1234', 'shell', '-x', 'logcat', '-v', 'time', '-t', '1',
]))
.thenReturn(ProcessResult(0, 0, '$kLastLogcatTimestamp I/flutter: irrelevant', ''));
final Completer<void> logcatCompleter = Completer<void>();
when(mockProcessManager.start(argThat(contains('logcat'))))
.thenAnswer((_) {
logcatCompleter.complete();
return Future<Process>.value(createMockProcess());
});
final AndroidDevice device = AndroidDevice('1234');
final DeviceLogReader logReader = device.getLogReader();
logReader.logLines.listen((_) {});
await logcatCompleter.future;
verify(mockProcessManager.start(<String>[
'adb', '-s', '1234', 'logcat', '-v', 'time',
if (apiVersion != null && apiVersion >= kLollipopVersionCode)
...<String>['-T', kLastLogcatTimestamp],
])).called(1);
}, overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
ProcessManager: () => mockProcessManager,
});
testUsingContext('calls adb logcat with expected flags when the device logs are empty, apiVersion=$apiVersion', () async {
setupGetprop(apiVersion: apiVersion);
when(mockAndroidSdk.adbPath).thenReturn('adb');
when(mockProcessManager.runSync(<String>[
'adb', '-s', '1234', 'shell', '-x', 'logcat', '-v', 'time', '-t', '1',
])).thenReturn(ProcessResult(0, 0, '', ''));
final Completer<void> logcatCompleter = Completer<void>();
when(mockProcessManager.start(argThat(contains('logcat'))))
.thenAnswer((_) {
logcatCompleter.complete();
return Future<Process>.value(createMockProcess());
});
final AndroidDevice device = AndroidDevice('1234');
final DeviceLogReader logReader = device.getLogReader();
logReader.logLines.listen((_) {});
await logcatCompleter.future;
verify(mockProcessManager.start(<String>[
'adb', '-s', '1234', 'logcat', '-v', 'time',
if (apiVersion != null && apiVersion >= kLollipopVersionCode)
...<String>['-T', ''],
])).called(1);
}, overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
ProcessManager: () => mockProcessManager,
});
}
callsAdbLogcatCorrectly(apiVersion: kLollipopVersionCode);
callsAdbLogcatCorrectly(apiVersion: kLollipopVersionCode - 1);
callsAdbLogcatCorrectly();
});
test('Can parse adb shell dumpsys info', () { test('Can parse adb shell dumpsys info', () {
const String exampleOutput = r''' const String exampleOutput = r'''
Applications Memory Usage (in Kilobytes): Applications Memory Usage (in Kilobytes):
......
...@@ -165,7 +165,7 @@ class _FakeProcess implements Process { ...@@ -165,7 +165,7 @@ class _FakeProcess implements Process {
@override @override
bool kill([io.ProcessSignal signal = io.ProcessSignal.sigterm]) { bool kill([io.ProcessSignal signal = io.ProcessSignal.sigterm]) {
assert(false, 'Process.kill() should not be used directly in flutter_tools.'); // Killing a fake process has no effect.
return false; return false;
} }
} }
...@@ -281,7 +281,7 @@ abstract class FakeProcessManager implements ProcessManager { ...@@ -281,7 +281,7 @@ abstract class FakeProcessManager implements ProcessManager {
@override @override
bool killPid(int pid, [io.ProcessSignal signal = io.ProcessSignal.sigterm]) { bool killPid(int pid, [io.ProcessSignal signal = io.ProcessSignal.sigterm]) {
assert(false, 'ProcessManager.killPid() should not be used directly in flutter_tools.'); // Killing a fake process has no effect.
return false; return false;
} }
} }
......
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