Unverified Commit 6a69f8c9 authored by Zachary Anderson's avatar Zachary Anderson Committed by GitHub

[fuchsia] Add support for the 'device' command using the SDK (#31910)

parent 06973f58
Ow5Xdviq7OwKr7XNuf-Bw0nBMeAr849mFn7gc_RUpzUC
mfXzGfxNWcf6BHsv083b56vQcj96yCo0exBFBdjE4gMC
......@@ -72,7 +72,7 @@ Future<T> runInContext<T>(
DoctorValidatorsProvider: () => DoctorValidatorsProvider.defaultInstance,
EmulatorManager: () => EmulatorManager(),
FuchsiaSdk: () => FuchsiaSdk(),
FuchsiaArtifacts: () => FuchsiaArtifacts(),
FuchsiaArtifacts: () => FuchsiaArtifacts.find(),
FuchsiaWorkflow: () => FuchsiaWorkflow(),
Flags: () => const EmptyFlags(),
FlutterVersion: () => FlutterVersion(const SystemClock()),
......
......@@ -35,7 +35,8 @@ Future<VMService> _kDefaultFuchsiaIsolateDiscoveryConnector(Uri uri) {
class _FuchsiaLogReader extends DeviceLogReader {
_FuchsiaLogReader(this._device, [this._app]);
static final RegExp _flutterLogOutput = RegExp(r'INFO: \w+\(flutter\): ');
// \S matches non-whitespace characters.
static final RegExp _flutterLogOutput = RegExp(r'INFO: \S+\(flutter\): ');
FuchsiaDevice _device;
ApplicationPackage _app;
......@@ -46,7 +47,7 @@ class _FuchsiaLogReader extends DeviceLogReader {
Stream<String> _logLines;
@override
Stream<String> get logLines {
_logLines ??= _processLogs(fuchsiaSdk.syslogs());
_logLines ??= _processLogs(fuchsiaSdk.syslogs(_device.id));
return _logLines;
}
......@@ -57,8 +58,8 @@ class _FuchsiaLogReader extends DeviceLogReader {
// Determine if line comes from flutter, and optionally whether it matches
// the correct fuchsia module.
final RegExp matchRegExp = _app == null
? _flutterLogOutput
: RegExp('INFO: ${_app.name}\\(flutter\\): ');
? _flutterLogOutput
: RegExp('INFO: ${_app.name}\\(flutter\\): ');
return Stream<String>.eventTransformed(
lines,
(Sink<String> outout) => _FuchsiaLogSink(outout, matchRegExp, startTime),
......@@ -90,16 +91,19 @@ class _FuchsiaLogSink implements EventSink<String> {
if (logTime.millisecondsSinceEpoch < _startTime.millisecondsSinceEpoch) {
return;
}
_outputSink.add('[${logTime.toLocal()}] Flutter: ${line.split(_matchRegExp).last}');
_outputSink.add(
'[${logTime.toLocal()}] Flutter: ${line.split(_matchRegExp).last}');
}
@override
void addError(Object error, [ StackTrace stackTrace ]) {
void addError(Object error, [StackTrace stackTrace]) {
_outputSink.addError(error, stackTrace);
}
@override
void close() { _outputSink.close(); }
void close() {
_outputSink.close();
}
}
class FuchsiaDevices extends PollingDeviceDiscovery {
......@@ -146,7 +150,7 @@ List<FuchsiaDevice> parseListDevices(String text) {
}
class FuchsiaDevice extends Device {
FuchsiaDevice(String id, { this.name }) : super(id);
FuchsiaDevice(String id, {this.name}) : super(id);
@override
bool get supportsHotReload => true;
......@@ -191,7 +195,8 @@ class FuchsiaDevice extends Device {
bool prebuiltApplication = false,
bool usesTerminalUi = true,
bool ipv6 = false,
}) => Future<void>.error('unimplemented');
}) =>
Future<void>.error('unimplemented');
@override
Future<bool> stopApp(ApplicationPackage app) async {
......@@ -206,15 +211,17 @@ class FuchsiaDevice extends Device {
Future<String> get sdkNameAndVersion async => 'Fuchsia';
@override
DeviceLogReader getLogReader({ ApplicationPackage app }) => _logReader ??= _FuchsiaLogReader(this, app);
DeviceLogReader getLogReader({ApplicationPackage app}) =>
_logReader ??= _FuchsiaLogReader(this, app);
_FuchsiaLogReader _logReader;
@override
DevicePortForwarder get portForwarder => _portForwarder ??= _FuchsiaPortForwarder(this);
DevicePortForwarder get portForwarder =>
_portForwarder ??= _FuchsiaPortForwarder(this);
_FuchsiaPortForwarder _portForwarder;
@override
void clearLogs() { }
void clearLogs() {}
@override
bool get supportsScreenshot => false;
......@@ -234,7 +241,8 @@ class FuchsiaDevice extends Device {
Future<List<int>> servicePorts() async {
final String findOutput = await shell('find /hub -name vmservice-port');
if (findOutput.trim() == '') {
throwToolExit('No Dart Observatories found. Are you running a debug build?');
throwToolExit(
'No Dart Observatories found. Are you running a debug build?');
return null;
}
final List<int> ports = <int>[];
......@@ -259,9 +267,15 @@ class FuchsiaDevice extends Device {
/// Run `command` on the Fuchsia device shell.
Future<String> shell(String command) async {
final RunResult result = await runAsync(<String>[
'ssh', '-F', fuchsiaArtifacts.sshConfig.absolute.path, id, command]);
'ssh',
'-F',
fuchsiaArtifacts.sshConfig.absolute.path,
id,
command
]);
if (result.exitCode != 0) {
throwToolExit('Command failed: $command\nstdout: ${result.stdout}\nstderr: ${result.stderr}');
throwToolExit(
'Command failed: $command\nstdout: ${result.stdout}\nstderr: ${result.stderr}');
return null;
}
return result.stdout;
......@@ -302,7 +316,9 @@ class FuchsiaDevice extends Device {
return null;
}
FuchsiaIsolateDiscoveryProtocol getIsolateDiscoveryProtocol(String isolateName) => FuchsiaIsolateDiscoveryProtocol(this, isolateName);
FuchsiaIsolateDiscoveryProtocol getIsolateDiscoveryProtocol(
String isolateName) =>
FuchsiaIsolateDiscoveryProtocol(this, isolateName);
@override
bool isSupportedForProject(FlutterProject flutterProject) => true;
......@@ -341,6 +357,7 @@ class FuchsiaIsolateDiscoveryProtocol {
return uri;
});
}
Uri _uri;
void dispose() {
......@@ -379,8 +396,8 @@ class FuchsiaIsolateDiscoveryProtocol {
final Uri address = flutterView.owner.vmService.httpAddress;
if (flutterView.uiIsolate.name.contains(_isolateName)) {
_foundUri.complete(_device.ipv6
? Uri.parse('http://[$_ipv6Loopback]:${address.port}/')
: Uri.parse('http://$_ipv4Loopback:${address.port}/'));
? Uri.parse('http://[$_ipv6Loopback]:${address.port}/')
: Uri.parse('http://$_ipv4Loopback:${address.port}/'));
_status.stop();
return;
}
......@@ -402,13 +419,22 @@ class _FuchsiaPortForwarder extends DevicePortForwarder {
final Map<int, Process> _processes = <int, Process>{};
@override
Future<int> forward(int devicePort, { int hostPort }) async {
Future<int> forward(int devicePort, {int hostPort}) async {
hostPort ??= await _findPort();
// Note: the provided command works around a bug in -N, see US-515
// for more explanation.
final List<String> command = <String>[
'ssh', '-6', '-F', fuchsiaArtifacts.sshConfig.absolute.path, '-nNT', '-vvv', '-f',
'-L', '$hostPort:$_ipv4Loopback:$devicePort', device.id, 'true',
'ssh',
'-6',
'-F',
fuchsiaArtifacts.sshConfig.absolute.path,
'-nNT',
'-vvv',
'-f',
'-L',
'$hostPort:$_ipv4Loopback:$devicePort',
device.id,
'true',
];
final Process process = await processManager.start(command);
unawaited(process.exitCode.then((int exitCode) {
......@@ -431,8 +457,16 @@ class _FuchsiaPortForwarder extends DevicePortForwarder {
final Process process = _processes.remove(forwardedPort.hostPort);
process?.kill();
final List<String> command = <String>[
'ssh', '-F', fuchsiaArtifacts.sshConfig.absolute.path, '-O', 'cancel', '-vvv',
'-L', '${forwardedPort.hostPort}:$_ipv4Loopback:${forwardedPort.devicePort}', device.id];
'ssh',
'-F',
fuchsiaArtifacts.sshConfig.absolute.path,
'-O',
'cancel',
'-vvv',
'-L',
'${forwardedPort.hostPort}:$_ipv4Loopback:${forwardedPort.devicePort}',
device.id
];
final ProcessResult result = await processManager.run(command);
if (result.exitCode != 0) {
throwToolExit(result.stderr);
......@@ -449,8 +483,9 @@ class _FuchsiaPortForwarder extends DevicePortForwarder {
// Failures are signaled by a return value of 0 from this function.
printTrace('_findPort failed: $e');
}
if (serverSocket != null)
if (serverSocket != null) {
await serverSocket.close();
}
return port;
}
}
......
......@@ -7,8 +7,10 @@ import 'dart:async';
import '../base/context.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/platform.dart';
import '../base/process.dart';
import '../base/process_manager.dart';
import '../cache.dart';
import '../convert.dart';
import '../globals.dart';
......@@ -23,8 +25,6 @@ FuchsiaArtifacts get fuchsiaArtifacts => context.get<FuchsiaArtifacts>();
/// This workflow assumes development within the fuchsia source tree,
/// including a working fx command-line tool in the user's PATH.
class FuchsiaSdk {
static const List<String> _syslogCommand = <String>['fx', 'syslog', '--clock', 'Local'];
/// Example output:
/// $ dev_finder list -full
/// > 192.168.42.56 paper-pulp-bush-angel
......@@ -42,19 +42,33 @@ class FuchsiaSdk {
/// Returns the fuchsia system logs for an attached device.
///
/// Does not currently support multiple attached devices.
Stream<String> syslogs() {
Stream<String> syslogs(String id) {
Process process;
try {
final StreamController<String> controller = StreamController<String>(onCancel: () {
final StreamController<String> controller =
StreamController<String>(onCancel: () {
process.kill();
});
processManager.start(_syslogCommand).then((Process newProcess) {
if (fuchsiaArtifacts.sshConfig == null) {
return null;
}
const String remoteCommand = 'log_listener --clock Local';
final List<String> cmd = <String>[
'ssh',
'-F',
fuchsiaArtifacts.sshConfig.absolute.path,
id,
remoteCommand
];
processManager.start(cmd).then((Process newProcess) {
if (controller.isClosed) {
return;
}
process = newProcess;
process.exitCode.whenComplete(controller.close);
controller.addStream(process.stdout.transform(utf8.decoder).transform(const LineSplitter()));
controller.addStream(process.stdout
.transform(utf8.decoder)
.transform(const LineSplitter()));
});
return controller.stream;
} catch (exception) {
......@@ -69,6 +83,35 @@ class FuchsiaArtifacts {
/// Creates a new [FuchsiaArtifacts].
FuchsiaArtifacts({this.sshConfig, this.devFinder});
/// Creates a new [FuchsiaArtifacts] using the cached Fuchsia SDK.
///
/// Finds tools under bin/cache/artifacts/fuchsia/tools.
/// Queries environment variables (first FUCHSIA_BUILD_DIR, then
/// FUCHSIA_SSH_CONFIG) to find the ssh configuration needed to talk to
/// a device.
factory FuchsiaArtifacts.find() {
final String fuchsia = Cache.instance.getArtifactDirectory('fuchsia').path;
final String tools = fs.path.join(fuchsia, 'tools');
// If FUCHSIA_BUILD_DIR is defined, then look for the ssh_config dir
// relative to it. Next, if FUCHSIA_SSH_CONFIG is defined, then use it.
// TODO(zra): Consider passing the ssh config path in with a flag.
File sshConfig;
if (platform.environment.containsKey(_kFuchsiaBuildDir)) {
sshConfig = fs.file(fs.path.join(
platform.environment[_kFuchsiaSshConfig], 'ssh-keys', 'ssh_config'));
} else if (platform.environment.containsKey(_kFuchsiaSshConfig)) {
sshConfig = fs.file(platform.environment[_kFuchsiaSshConfig]);
}
return FuchsiaArtifacts(
sshConfig: sshConfig,
devFinder: fs.file(fs.path.join(tools, 'dev_finder')),
);
}
static const String _kFuchsiaSshConfig = 'FUCHSIA_SSH_CONFIG';
static const String _kFuchsiaBuildDir = 'FUCHSIA_BUILD_DIR';
/// The location of the SSH configuration file used to interact with a
/// Fuchsia device.
final File sshConfig;
......
......@@ -66,7 +66,8 @@ void main() {
any,
environment: anyNamed('environment'),
workingDirectory: anyNamed('workingDirectory'),
)).thenAnswer((Invocation invocation) => Future<ProcessResult>.value(mockProcessResult));
)).thenAnswer((Invocation invocation) =>
Future<ProcessResult>.value(mockProcessResult));
when(mockProcessResult.exitCode).thenReturn(1);
when<String>(mockProcessResult.stdout).thenReturn('');
when<String>(mockProcessResult.stderr).thenReturn('');
......@@ -79,7 +80,8 @@ void main() {
any,
environment: anyNamed('environment'),
workingDirectory: anyNamed('workingDirectory'),
)).thenAnswer((Invocation invocation) => Future<ProcessResult>.value(emptyStdoutProcessResult));
)).thenAnswer((Invocation invocation) =>
Future<ProcessResult>.value(emptyStdoutProcessResult));
when(emptyStdoutProcessResult.exitCode).thenReturn(0);
when<String>(emptyStdoutProcessResult.stdout).thenReturn('');
when<String>(emptyStdoutProcessResult.stderr).thenReturn('');
......@@ -92,23 +94,26 @@ void main() {
} on ToolExit catch (err) {
toolExit = err;
}
expect(toolExit.message, contains('No Dart Observatories found. Are you running a debug build?'));
expect(
toolExit.message,
contains(
'No Dart Observatories found. Are you running a debug build?'));
}, overrides: <Type, Generator>{
ProcessManager: () => emptyStdoutProcessManager,
FuchsiaArtifacts: () => FuchsiaArtifacts(
sshConfig: mockFile,
devFinder: mockFile,
),
sshConfig: mockFile,
devFinder: mockFile,
),
});
group('device logs', () {
const String exampleUtcLogs = '''
[2018-11-09 01:27:45][3][297950920][log] INFO: example_app(flutter): Error doing thing
[2018-11-09 01:27:45][3][297950920][log] INFO: example_app.cmx(flutter): Error doing thing
[2018-11-09 01:27:58][46257][46269][foo] INFO: Using a thing
[2018-11-09 01:29:58][46257][46269][foo] INFO: Blah blah blah
[2018-11-09 01:29:58][46257][46269][foo] INFO: other_app(flutter): Do thing
[2018-11-09 01:29:58][46257][46269][foo] INFO: other_app.cmx(flutter): Do thing
[2018-11-09 01:30:02][41175][41187][bar] INFO: Invoking a bar
[2018-11-09 01:30:12][52580][52983][log] INFO: example_app(flutter): Did thing this time
[2018-11-09 01:30:12][52580][52983][log] INFO: example_app.cmx(flutter): Did thing this time
''';
final MockProcessManager mockProcessManager = MockProcessManager();
......@@ -116,11 +121,17 @@ void main() {
Completer<int> exitCode;
StreamController<List<int>> stdout;
StreamController<List<int>> stderr;
when(mockProcessManager.start(any)).thenAnswer((Invocation _) => Future<Process>.value(mockProcess));
when(mockProcessManager.start(any))
.thenAnswer((Invocation _) => Future<Process>.value(mockProcess));
when(mockProcess.exitCode).thenAnswer((Invocation _) => exitCode.future);
when(mockProcess.stdout).thenAnswer((Invocation _) => stdout.stream);
when(mockProcess.stderr).thenAnswer((Invocation _) => stderr.stream);
final MockFile devFinder = MockFile();
final MockFile sshConfig = MockFile();
when(devFinder.absolute).thenReturn(devFinder);
when(sshConfig.absolute).thenReturn(sshConfig);
setUp(() {
stdout = StreamController<List<int>>(sync: true);
stderr = StreamController<List<int>>(sync: true);
......@@ -133,7 +144,8 @@ void main() {
testUsingContext('can be parsed for an app', () async {
final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester');
final DeviceLogReader reader = device.getLogReader(app: FuchsiaModulePackage(name: 'example_app'));
final DeviceLogReader reader = device.getLogReader(
app: FuchsiaModulePackage(name: 'example_app.cmx'));
final List<String> logLines = <String>[];
final Completer<void> lock = Completer<void>();
reader.logLines.listen((String line) {
......@@ -155,11 +167,14 @@ void main() {
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
SystemClock: () => SystemClock.fixed(DateTime(2018, 11, 9, 1, 25, 45)),
FuchsiaArtifacts: () =>
FuchsiaArtifacts(devFinder: devFinder, sshConfig: sshConfig),
});
testUsingContext('cuts off prior logs', () async {
final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester');
final DeviceLogReader reader = device.getLogReader(app: FuchsiaModulePackage(name: 'example_app'));
final DeviceLogReader reader = device.getLogReader(
app: FuchsiaModulePackage(name: 'example_app.cmx'));
final List<String> logLines = <String>[];
final Completer<void> lock = Completer<void>();
reader.logLines.listen((String line) {
......@@ -178,6 +193,8 @@ void main() {
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
SystemClock: () => SystemClock.fixed(DateTime(2018, 11, 9, 1, 29, 45)),
FuchsiaArtifacts: () =>
FuchsiaArtifacts(devFinder: devFinder, sshConfig: sshConfig),
});
testUsingContext('can be parsed for all apps', () async {
......@@ -205,12 +222,15 @@ void main() {
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
SystemClock: () => SystemClock.fixed(DateTime(2018, 11, 9, 1, 25, 45)),
FuchsiaArtifacts: () =>
FuchsiaArtifacts(devFinder: devFinder, sshConfig: sshConfig),
});
});
});
group(FuchsiaIsolateDiscoveryProtocol, () {
Future<Uri> findUri(List<MockFlutterView> views, String expectedIsolateName) {
Future<Uri> findUri(
List<MockFlutterView> views, String expectedIsolateName) {
final MockPortForwarder portForwarder = MockPortForwarder();
final MockVMService vmService = MockVMService();
final MockVM vm = MockVM();
......@@ -220,33 +240,43 @@ void main() {
for (MockFlutterView view in views) {
view.owner = vm;
}
final MockFuchsiaDevice fuchsiaDevice = MockFuchsiaDevice('123', portForwarder, false);
final FuchsiaIsolateDiscoveryProtocol discoveryProtocol = FuchsiaIsolateDiscoveryProtocol(
final MockFuchsiaDevice fuchsiaDevice =
MockFuchsiaDevice('123', portForwarder, false);
final FuchsiaIsolateDiscoveryProtocol discoveryProtocol =
FuchsiaIsolateDiscoveryProtocol(
fuchsiaDevice,
expectedIsolateName,
(Uri uri) async => vmService,
true, // only poll once.
);
when(fuchsiaDevice.servicePorts()).thenAnswer((Invocation invocation) async => <int>[1]);
when(portForwarder.forward(1)).thenAnswer((Invocation invocation) async => 2);
when(vmService.getVM()).thenAnswer((Invocation invocation) => Future<void>.value(null));
when(vmService.refreshViews()).thenAnswer((Invocation invocation) => Future<void>.value(null));
when(fuchsiaDevice.servicePorts())
.thenAnswer((Invocation invocation) async => <int>[1]);
when(portForwarder.forward(1))
.thenAnswer((Invocation invocation) async => 2);
when(vmService.getVM())
.thenAnswer((Invocation invocation) => Future<void>.value(null));
when(vmService.refreshViews())
.thenAnswer((Invocation invocation) => Future<void>.value(null));
when(vmService.httpAddress).thenReturn(Uri.parse('example'));
return discoveryProtocol.uri;
}
testUsingContext('can find flutter view with matching isolate name', () async {
testUsingContext('can find flutter view with matching isolate name',
() async {
const String expectedIsolateName = 'foobar';
final Uri uri = await findUri(<MockFlutterView>[
MockFlutterView(null), // no ui isolate.
MockFlutterView(MockIsolate('wrong name')), // wrong name.
MockFlutterView(MockIsolate(expectedIsolateName)), // matching name.
], expectedIsolateName);
expect(uri.toString(), 'http://${InternetAddress.loopbackIPv4.address}:0/');
expect(
uri.toString(), 'http://${InternetAddress.loopbackIPv4.address}:0/');
}, overrides: <Type, Generator>{
Logger: () => StdoutLogger(),
});
testUsingContext('can handle flutter view without matching isolate name', () async {
testUsingContext('can handle flutter view without matching isolate name',
() async {
const String expectedIsolateName = 'foobar';
final Future<Uri> uri = findUri(<MockFlutterView>[
MockFlutterView(null), // no ui isolate.
......
......@@ -12,37 +12,47 @@ import '../src/common.dart';
import '../src/context.dart';
class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils {}
class MockFile extends Mock implements File {}
void main() {
group('android workflow', () {
group('Fuchsia workflow', () {
final MockFile devFinder = MockFile();
final MockFile sshConfig = MockFile();
when(devFinder.absolute).thenReturn(devFinder);
when(sshConfig.absolute).thenReturn(sshConfig);
testUsingContext('can not list and launch devices if there is not ssh config and dev finder', () {
testUsingContext(
'can not list and launch devices if there is not ssh config and dev finder',
() {
expect(fuchsiaWorkflow.canLaunchDevices, false);
expect(fuchsiaWorkflow.canListDevices, false);
expect(fuchsiaWorkflow.canListEmulators, false);
}, overrides: <Type, Generator>{
FuchsiaArtifacts: () => FuchsiaArtifacts(devFinder: null, sshConfig: null),
FuchsiaArtifacts: () =>
FuchsiaArtifacts(devFinder: null, sshConfig: null),
});
testUsingContext('can not list and launch devices if there is not ssh config and dev finder', () {
testUsingContext(
'can not list and launch devices if there is not ssh config and dev finder',
() {
expect(fuchsiaWorkflow.canLaunchDevices, false);
expect(fuchsiaWorkflow.canListDevices, true);
expect(fuchsiaWorkflow.canListEmulators, false);
}, overrides: <Type, Generator>{
FuchsiaArtifacts: () => FuchsiaArtifacts(devFinder: devFinder, sshConfig: null),
FuchsiaArtifacts: () =>
FuchsiaArtifacts(devFinder: devFinder, sshConfig: null),
});
testUsingContext('can list and launch devices supported if there is a `fx` command', () {
testUsingContext(
'can list and launch devices supported with sufficient SDK artifacts',
() {
expect(fuchsiaWorkflow.canLaunchDevices, true);
expect(fuchsiaWorkflow.canListDevices, true);
expect(fuchsiaWorkflow.canListEmulators, false);
}, overrides: <Type, Generator>{
FuchsiaArtifacts: () => FuchsiaArtifacts(devFinder: devFinder, sshConfig: sshConfig),
FuchsiaArtifacts: () =>
FuchsiaArtifacts(devFinder: devFinder, sshConfig: sshConfig),
});
});
}
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