diff --git a/bin/internal/fuchsia.version b/bin/internal/fuchsia.version index d6f6c55370e..8e9ecb96973 100644 --- a/bin/internal/fuchsia.version +++ b/bin/internal/fuchsia.version @@ -1 +1 @@ -Ow5Xdviq7OwKr7XNuf-Bw0nBMeAr849mFn7gc_RUpzUC +mfXzGfxNWcf6BHsv083b56vQcj96yCo0exBFBdjE4gMC diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart index 2e253a9be19..eb7dea599aa 100644 --- a/packages/flutter_tools/lib/src/context_runner.dart +++ b/packages/flutter_tools/lib/src/context_runner.dart @@ -72,7 +72,7 @@ Future runInContext( DoctorValidatorsProvider: () => DoctorValidatorsProvider.defaultInstance, EmulatorManager: () => EmulatorManager(), FuchsiaSdk: () => FuchsiaSdk(), - FuchsiaArtifacts: () => FuchsiaArtifacts(), + FuchsiaArtifacts: () => FuchsiaArtifacts.find(), FuchsiaWorkflow: () => FuchsiaWorkflow(), Flags: () => const EmptyFlags(), FlutterVersion: () => FlutterVersion(const SystemClock()), diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart index c7b1ba8b506..a2fddc26dfc 100644 --- a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart +++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart @@ -35,7 +35,8 @@ Future _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 _logLines; @override Stream 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.eventTransformed( lines, (Sink outout) => _FuchsiaLogSink(outout, matchRegExp, startTime), @@ -90,16 +91,19 @@ class _FuchsiaLogSink implements EventSink { 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 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.error('unimplemented'); + }) => + Future.error('unimplemented'); @override Future stopApp(ApplicationPackage app) async { @@ -206,15 +211,17 @@ class FuchsiaDevice extends Device { Future 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> 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 ports = []; @@ -259,9 +267,15 @@ class FuchsiaDevice extends Device { /// Run `command` on the Fuchsia device shell. Future shell(String command) async { final RunResult result = await runAsync([ - '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 _processes = {}; @override - Future forward(int devicePort, { int hostPort }) async { + Future 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 command = [ - '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 command = [ - '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; } } diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_sdk.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_sdk.dart index e97c56f9228..b246a098bc8 100644 --- a/packages/flutter_tools/lib/src/fuchsia/fuchsia_sdk.dart +++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_sdk.dart @@ -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(); /// 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 _syslogCommand = ['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 syslogs() { + Stream syslogs(String id) { Process process; try { - final StreamController controller = StreamController(onCancel: () { + final StreamController controller = + StreamController(onCancel: () { process.kill(); }); - processManager.start(_syslogCommand).then((Process newProcess) { + if (fuchsiaArtifacts.sshConfig == null) { + return null; + } + const String remoteCommand = 'log_listener --clock Local'; + final List cmd = [ + '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; diff --git a/packages/flutter_tools/test/fuchsia/fuchsa_device_test.dart b/packages/flutter_tools/test/fuchsia/fuchsa_device_test.dart index 7851f88b57f..e7d3f51003f 100644 --- a/packages/flutter_tools/test/fuchsia/fuchsa_device_test.dart +++ b/packages/flutter_tools/test/fuchsia/fuchsa_device_test.dart @@ -66,7 +66,8 @@ void main() { any, environment: anyNamed('environment'), workingDirectory: anyNamed('workingDirectory'), - )).thenAnswer((Invocation invocation) => Future.value(mockProcessResult)); + )).thenAnswer((Invocation invocation) => + Future.value(mockProcessResult)); when(mockProcessResult.exitCode).thenReturn(1); when(mockProcessResult.stdout).thenReturn(''); when(mockProcessResult.stderr).thenReturn(''); @@ -79,7 +80,8 @@ void main() { any, environment: anyNamed('environment'), workingDirectory: anyNamed('workingDirectory'), - )).thenAnswer((Invocation invocation) => Future.value(emptyStdoutProcessResult)); + )).thenAnswer((Invocation invocation) => + Future.value(emptyStdoutProcessResult)); when(emptyStdoutProcessResult.exitCode).thenReturn(0); when(emptyStdoutProcessResult.stdout).thenReturn(''); when(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: { 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 exitCode; StreamController> stdout; StreamController> stderr; - when(mockProcessManager.start(any)).thenAnswer((Invocation _) => Future.value(mockProcess)); + when(mockProcessManager.start(any)) + .thenAnswer((Invocation _) => Future.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>(sync: true); stderr = StreamController>(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 logLines = []; final Completer lock = Completer(); reader.logLines.listen((String line) { @@ -155,11 +167,14 @@ void main() { }, overrides: { 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 logLines = []; final Completer lock = Completer(); reader.logLines.listen((String line) { @@ -178,6 +193,8 @@ void main() { }, overrides: { 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: { ProcessManager: () => mockProcessManager, SystemClock: () => SystemClock.fixed(DateTime(2018, 11, 9, 1, 25, 45)), + FuchsiaArtifacts: () => + FuchsiaArtifacts(devFinder: devFinder, sshConfig: sshConfig), }); }); }); group(FuchsiaIsolateDiscoveryProtocol, () { - Future findUri(List views, String expectedIsolateName) { + Future findUri( + List 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 => [1]); - when(portForwarder.forward(1)).thenAnswer((Invocation invocation) async => 2); - when(vmService.getVM()).thenAnswer((Invocation invocation) => Future.value(null)); - when(vmService.refreshViews()).thenAnswer((Invocation invocation) => Future.value(null)); + when(fuchsiaDevice.servicePorts()) + .thenAnswer((Invocation invocation) async => [1]); + when(portForwarder.forward(1)) + .thenAnswer((Invocation invocation) async => 2); + when(vmService.getVM()) + .thenAnswer((Invocation invocation) => Future.value(null)); + when(vmService.refreshViews()) + .thenAnswer((Invocation invocation) => Future.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(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: { 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 = findUri([ MockFlutterView(null), // no ui isolate. diff --git a/packages/flutter_tools/test/fuchsia/fuchsia_workflow_test.dart b/packages/flutter_tools/test/fuchsia/fuchsia_workflow_test.dart index f7c2a4cf9dc..7c456da791c 100644 --- a/packages/flutter_tools/test/fuchsia/fuchsia_workflow_test.dart +++ b/packages/flutter_tools/test/fuchsia/fuchsia_workflow_test.dart @@ -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: { - 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: { - 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: { - FuchsiaArtifacts: () => FuchsiaArtifacts(devFinder: devFinder, sshConfig: sshConfig), + FuchsiaArtifacts: () => + FuchsiaArtifacts(devFinder: devFinder, sshConfig: sshConfig), }); }); }