• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2017 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6
7import 'package:meta/meta.dart';
8
9import '../application_package.dart';
10import '../artifacts.dart';
11import '../base/common.dart';
12import '../base/context.dart';
13import '../base/file_system.dart';
14import '../base/io.dart';
15import '../base/logger.dart';
16import '../base/os.dart';
17import '../base/platform.dart';
18import '../base/process.dart';
19import '../base/process_manager.dart';
20import '../base/time.dart';
21import '../build_info.dart';
22import '../device.dart';
23import '../globals.dart';
24import '../project.dart';
25import '../vmservice.dart';
26
27import 'amber_ctl.dart';
28import 'application_package.dart';
29import 'fuchsia_build.dart';
30import 'fuchsia_pm.dart';
31import 'fuchsia_sdk.dart';
32import 'fuchsia_workflow.dart';
33import 'tiles_ctl.dart';
34
35/// The [FuchsiaDeviceTools] instance.
36FuchsiaDeviceTools get fuchsiaDeviceTools => context.get<FuchsiaDeviceTools>();
37
38/// Fuchsia device-side tools.
39class FuchsiaDeviceTools {
40  FuchsiaAmberCtl _amberCtl;
41  FuchsiaAmberCtl get amberCtl => _amberCtl ??= FuchsiaAmberCtl();
42
43  FuchsiaTilesCtl _tilesCtl;
44  FuchsiaTilesCtl get tilesCtl => _tilesCtl ??= FuchsiaTilesCtl();
45}
46
47final String _ipv4Loopback = InternetAddress.loopbackIPv4.address;
48final String _ipv6Loopback = InternetAddress.loopbackIPv6.address;
49
50// Enables testing the fuchsia isolate discovery
51Future<VMService> _kDefaultFuchsiaIsolateDiscoveryConnector(Uri uri) {
52  return VMService.connect(uri);
53}
54
55/// Read the log for a particular device.
56class _FuchsiaLogReader extends DeviceLogReader {
57  _FuchsiaLogReader(this._device, [this._app]);
58
59  // \S matches non-whitespace characters.
60  static final RegExp _flutterLogOutput = RegExp(r'INFO: \S+\(flutter\): ');
61
62  final FuchsiaDevice _device;
63  final ApplicationPackage _app;
64
65  @override
66  String get name => _device.name;
67
68  Stream<String> _logLines;
69  @override
70  Stream<String> get logLines {
71    final Stream<String> logStream = fuchsiaSdk.syslogs(_device.id);
72    _logLines ??= _processLogs(logStream);
73    return _logLines;
74  }
75
76  Stream<String> _processLogs(Stream<String> lines) {
77    if (lines == null) {
78      return null;
79    }
80    // Get the starting time of the log processor to filter logs from before
81    // the process attached.
82    final DateTime startTime = systemClock.now();
83    // Determine if line comes from flutter, and optionally whether it matches
84    // the correct fuchsia module.
85    final RegExp matchRegExp = _app == null
86        ? _flutterLogOutput
87        : RegExp('INFO: ${_app.name}(\.cmx)?\\(flutter\\): ');
88    return Stream<String>.eventTransformed(
89      lines,
90      (Sink<String> outout) => _FuchsiaLogSink(outout, matchRegExp, startTime),
91    );
92  }
93
94  @override
95  String toString() => name;
96}
97
98class _FuchsiaLogSink implements EventSink<String> {
99  _FuchsiaLogSink(this._outputSink, this._matchRegExp, this._startTime);
100
101  static final RegExp _utcDateOutput = RegExp(r'\d+\-\d+\-\d+ \d+:\d+:\d+');
102  final EventSink<String> _outputSink;
103  final RegExp _matchRegExp;
104  final DateTime _startTime;
105
106  @override
107  void add(String line) {
108    if (!_matchRegExp.hasMatch(line)) {
109      return;
110    }
111    final String rawDate = _utcDateOutput.firstMatch(line)?.group(0);
112    if (rawDate == null) {
113      return;
114    }
115    final DateTime logTime = DateTime.parse(rawDate);
116    if (logTime.millisecondsSinceEpoch < _startTime.millisecondsSinceEpoch) {
117      return;
118    }
119    _outputSink.add(
120        '[${logTime.toLocal()}] Flutter: ${line.split(_matchRegExp).last}');
121  }
122
123  @override
124  void addError(Object error, [StackTrace stackTrace]) {
125    _outputSink.addError(error, stackTrace);
126  }
127
128  @override
129  void close() {
130    _outputSink.close();
131  }
132}
133
134class FuchsiaDevices extends PollingDeviceDiscovery {
135  FuchsiaDevices() : super('Fuchsia devices');
136
137  @override
138  bool get supportsPlatform => platform.isLinux || platform.isMacOS;
139
140  @override
141  bool get canListAnything => fuchsiaWorkflow.canListDevices;
142
143  @override
144  Future<List<Device>> pollingGetDevices() async {
145    if (!fuchsiaWorkflow.canListDevices) {
146      return <Device>[];
147    }
148    final String text = await fuchsiaSdk.listDevices();
149    if (text == null || text.isEmpty) {
150      return <Device>[];
151    }
152    final List<FuchsiaDevice> devices = parseListDevices(text);
153    return devices;
154  }
155
156  @override
157  Future<List<String>> getDiagnostics() async => const <String>[];
158}
159
160@visibleForTesting
161List<FuchsiaDevice> parseListDevices(String text) {
162  final List<FuchsiaDevice> devices = <FuchsiaDevice>[];
163  for (String rawLine in text.trim().split('\n')) {
164    final String line = rawLine.trim();
165    // ['ip', 'device name']
166    final List<String> words = line.split(' ');
167    if (words.length < 2) {
168      continue;
169    }
170    final String name = words[1];
171    final String id = words[0];
172    devices.add(FuchsiaDevice(id, name: name));
173  }
174  return devices;
175}
176
177class FuchsiaDevice extends Device {
178  FuchsiaDevice(String id, {this.name}) : super(
179      id,
180      platformType: PlatformType.fuchsia,
181      category: null,
182      ephemeral: false,
183  );
184
185  @override
186  bool get supportsHotReload => true;
187
188  @override
189  bool get supportsHotRestart => false;
190
191  @override
192  bool get supportsFlutterExit => false;
193
194  @override
195  final String name;
196
197  @override
198  Future<bool> get isLocalEmulator async => false;
199
200  @override
201  Future<String> get emulatorId async => null;
202
203  @override
204  bool get supportsStartPaused => false;
205
206  @override
207  Future<bool> isAppInstalled(ApplicationPackage app) async => false;
208
209  @override
210  Future<bool> isLatestBuildInstalled(ApplicationPackage app) async => false;
211
212  @override
213  Future<bool> installApp(ApplicationPackage app) => Future<bool>.value(false);
214
215  @override
216  Future<bool> uninstallApp(ApplicationPackage app) async => false;
217
218  @override
219  bool isSupported() => true;
220
221  @override
222  Future<LaunchResult> startApp(
223    covariant FuchsiaApp package, {
224    String mainPath,
225    String route,
226    DebuggingOptions debuggingOptions,
227    Map<String, dynamic> platformArgs,
228    bool prebuiltApplication = false,
229    bool usesTerminalUi = true,
230    bool ipv6 = false,
231  }) async {
232    if (!prebuiltApplication) {
233      await buildFuchsia(fuchsiaProject: FlutterProject.current().fuchsia,
234                         target: mainPath,
235                         buildInfo: debuggingOptions.buildInfo);
236    }
237    // Stop the app if it's currently running.
238    await stopApp(package);
239    // Find out who the device thinks we are.
240    final String host = await fuchsiaSdk.fuchsiaDevFinder.resolve(name);
241    if (host == null) {
242      printError('Failed to resolve host for Fuchsia device');
243      return LaunchResult.failed();
244    }
245    final int port = await os.findFreePort();
246    if (port == 0) {
247      printError('Failed to find a free port');
248      return LaunchResult.failed();
249    }
250    final Directory packageRepo =
251        fs.directory(fs.path.join(getFuchsiaBuildDirectory(), '.pkg-repo'));
252    packageRepo.createSync(recursive: true);
253
254    final String appName = FlutterProject.current().manifest.appName;
255
256    final Status status = logger.startProgress(
257      'Starting Fuchsia application...',
258      timeout: null,
259    );
260    FuchsiaPackageServer fuchsiaPackageServer;
261    bool serverRegistered = false;
262    try {
263      // Ask amber to pre-fetch some things we'll need before setting up our own
264      // package server. This is to avoid relying on amber correctly using
265      // multiple package servers, support for which is in flux.
266      if (!await fuchsiaDeviceTools.amberCtl.getUp(this, 'tiles')) {
267        printError('Failed to get amber to prefetch tiles');
268        return LaunchResult.failed();
269      }
270      if (!await fuchsiaDeviceTools.amberCtl.getUp(this, 'tiles_ctl')) {
271        printError('Failed to get amber to prefetch tiles_ctl');
272        return LaunchResult.failed();
273      }
274
275      // Start up a package server.
276      const String packageServerName = 'flutter_tool';
277      fuchsiaPackageServer = FuchsiaPackageServer(
278          packageRepo.path, packageServerName, host, port);
279      if (!await fuchsiaPackageServer.start()) {
280        printError('Failed to start the Fuchsia package server');
281        return LaunchResult.failed();
282      }
283      final File farArchive = package.farArchive(
284          debuggingOptions.buildInfo.mode);
285      if (!await fuchsiaPackageServer.addPackage(farArchive)) {
286        printError('Failed to add package to the package server');
287        return LaunchResult.failed();
288      }
289
290      // Teach the package controller about the package server.
291      if (!await fuchsiaDeviceTools.amberCtl.addRepoCfg(this, fuchsiaPackageServer)) {
292        printError('Failed to teach amber about the package server');
293        return LaunchResult.failed();
294      }
295      serverRegistered = true;
296
297      // Tell the package controller to prefetch the app.
298      if (!await fuchsiaDeviceTools.amberCtl.pkgCtlResolve(
299          this, fuchsiaPackageServer, appName)) {
300        printError('Failed to get pkgctl to prefetch the package');
301        return LaunchResult.failed();
302      }
303
304      // Ensure tiles_ctl is started, and start the app.
305      if (!await FuchsiaTilesCtl.ensureStarted(this)) {
306        printError('Failed to ensure that tiles is started on the device');
307        return LaunchResult.failed();
308      }
309
310      // Instruct tiles_ctl to start the app.
311      final String fuchsiaUrl =
312          'fuchsia-pkg://$packageServerName/$appName#meta/$appName.cmx';
313      if (!await fuchsiaDeviceTools.tilesCtl.add(this, fuchsiaUrl, <String>[])) {
314        printError('Failed to add the app to tiles');
315        return LaunchResult.failed();
316      }
317    } finally {
318      // Try to un-teach the package controller about the package server if
319      // needed.
320      if (serverRegistered) {
321        await fuchsiaDeviceTools.amberCtl.pkgCtlRepoRemove(this, fuchsiaPackageServer);
322      }
323      // Shutdown the package server and delete the package repo;
324      fuchsiaPackageServer?.stop();
325      packageRepo.deleteSync(recursive: true);
326      status.cancel();
327    }
328
329    if (!debuggingOptions.buildInfo.isDebug &&
330        !debuggingOptions.buildInfo.isProfile) {
331      return LaunchResult.succeeded();
332    }
333
334    // In a debug or profile build, try to find the observatory uri.
335    final FuchsiaIsolateDiscoveryProtocol discovery =
336        getIsolateDiscoveryProtocol(appName);
337    try {
338      final Uri observatoryUri = await discovery.uri;
339      return LaunchResult.succeeded(observatoryUri: observatoryUri);
340    } finally {
341      discovery.dispose();
342    }
343  }
344
345  @override
346  Future<bool> stopApp(covariant FuchsiaApp app) async {
347    final int appKey = await FuchsiaTilesCtl.findAppKey(this, app.id);
348    if (appKey != -1) {
349      if (!await fuchsiaDeviceTools.tilesCtl.remove(this, appKey)) {
350        printError('tiles_ctl remove on ${app.id} failed.');
351        return false;
352      }
353    }
354    return true;
355  }
356
357  @override
358  Future<TargetPlatform> get targetPlatform async => TargetPlatform.fuchsia;
359
360  @override
361  Future<String> get sdkNameAndVersion async {
362    const String versionPath = '/pkgfs/packages/build-info/0/data/version';
363    final RunResult catResult = await shell('cat $versionPath');
364    if (catResult.exitCode != 0) {
365      printTrace('Failed to cat $versionPath: ${catResult.stderr}');
366      return 'Fuchsia';
367    }
368    final String version = catResult.stdout.trim();
369    if (version.isEmpty) {
370      printTrace('$versionPath was empty');
371      return 'Fuchsia';
372    }
373    return 'Fuchsia $version';
374  }
375
376  @override
377  DeviceLogReader getLogReader({ApplicationPackage app}) =>
378      _logReader ??= _FuchsiaLogReader(this, app);
379  _FuchsiaLogReader _logReader;
380
381  @override
382  DevicePortForwarder get portForwarder =>
383      _portForwarder ??= _FuchsiaPortForwarder(this);
384  _FuchsiaPortForwarder _portForwarder;
385
386  @override
387  void clearLogs() {}
388
389  @override
390  OverrideArtifacts get artifactOverrides {
391    return _artifactOverrides ??= OverrideArtifacts(
392      parent: Artifacts.instance,
393      platformKernelDill: fuchsiaArtifacts.platformKernelDill,
394      flutterPatchedSdk: fuchsiaArtifacts.flutterPatchedSdk,
395    );
396  }
397  OverrideArtifacts _artifactOverrides;
398
399  @override
400  bool get supportsScreenshot => false;
401
402  bool get ipv6 {
403    // Workaround for https://github.com/dart-lang/sdk/issues/29456
404    final String fragment = id.split('%').first;
405    try {
406      Uri.parseIPv6Address(fragment);
407      return true;
408    } on FormatException {
409      return false;
410    }
411  }
412
413  /// List the ports currently running a dart observatory.
414  Future<List<int>> servicePorts() async {
415    const String findCommand = 'find /hub -name vmservice-port';
416    final RunResult findResult = await shell(findCommand);
417    if (findResult.exitCode != 0) {
418      throwToolExit("'$findCommand' on device $id failed. stderr: '${findResult.stderr}'");
419      return null;
420    }
421    final String findOutput = findResult.stdout;
422    if (findOutput.trim() == '') {
423      throwToolExit(
424          'No Dart Observatories found. Are you running a debug build?');
425      return null;
426    }
427    final List<int> ports = <int>[];
428    for (String path in findOutput.split('\n')) {
429      if (path == '') {
430        continue;
431      }
432      final String lsCommand = 'ls $path';
433      final RunResult lsResult = await shell(lsCommand);
434      if (lsResult.exitCode != 0) {
435        throwToolExit("'$lsCommand' on device $id failed");
436        return null;
437      }
438      final String lsOutput = lsResult.stdout;
439      for (String line in lsOutput.split('\n')) {
440        if (line == '') {
441          continue;
442        }
443        final int port = int.tryParse(line);
444        if (port != null) {
445          ports.add(port);
446        }
447      }
448    }
449    return ports;
450  }
451
452  /// Run `command` on the Fuchsia device shell.
453  Future<RunResult> shell(String command) async {
454    if (fuchsiaArtifacts.sshConfig == null) {
455      throwToolExit('Cannot interact with device. No ssh config.\n'
456                    'Try setting FUCHSIA_SSH_CONFIG or FUCHSIA_BUILD_DIR.');
457    }
458    return await runAsync(<String>[
459      'ssh',
460      '-F',
461      fuchsiaArtifacts.sshConfig.absolute.path,
462      id,
463      command
464    ]);
465  }
466
467  /// Finds the first port running a VM matching `isolateName` from the
468  /// provided set of `ports`.
469  ///
470  /// Returns null if no isolate port can be found.
471  ///
472  // TODO(jonahwilliams): replacing this with the hub will require an update
473  // to the flutter_runner.
474  Future<int> findIsolatePort(String isolateName, List<int> ports) async {
475    for (int port in ports) {
476      try {
477        // Note: The square-bracket enclosure for using the IPv6 loopback
478        // didn't appear to work, but when assigning to the IPv4 loopback device,
479        // netstat shows that the local port is actually being used on the IPv6
480        // loopback (::1).
481        final Uri uri = Uri.parse('http://[$_ipv6Loopback]:$port');
482        final VMService vmService = await VMService.connect(uri);
483        await vmService.getVM();
484        await vmService.refreshViews();
485        for (FlutterView flutterView in vmService.vm.views) {
486          if (flutterView.uiIsolate == null) {
487            continue;
488          }
489          final Uri address = flutterView.owner.vmService.httpAddress;
490          if (flutterView.uiIsolate.name.contains(isolateName)) {
491            return address.port;
492          }
493        }
494      } on SocketException catch (err) {
495        printTrace('Failed to connect to $port: $err');
496      }
497    }
498    throwToolExit('No ports found running $isolateName');
499    return null;
500  }
501
502  FuchsiaIsolateDiscoveryProtocol getIsolateDiscoveryProtocol(
503          String isolateName) =>
504      FuchsiaIsolateDiscoveryProtocol(this, isolateName);
505
506  @override
507  bool isSupportedForProject(FlutterProject flutterProject) {
508    return flutterProject.fuchsia.existsSync();
509  }
510}
511
512class FuchsiaIsolateDiscoveryProtocol {
513  FuchsiaIsolateDiscoveryProtocol(
514    this._device,
515    this._isolateName, [
516    this._vmServiceConnector = _kDefaultFuchsiaIsolateDiscoveryConnector,
517    this._pollOnce = false,
518  ]);
519
520  static const Duration _pollDuration = Duration(seconds: 10);
521  final Map<int, VMService> _ports = <int, VMService>{};
522  final FuchsiaDevice _device;
523  final String _isolateName;
524  final Completer<Uri> _foundUri = Completer<Uri>();
525  final Future<VMService> Function(Uri) _vmServiceConnector;
526  // whether to only poll once.
527  final bool _pollOnce;
528  Timer _pollingTimer;
529  Status _status;
530
531  FutureOr<Uri> get uri {
532    if (_uri != null) {
533      return _uri;
534    }
535    _status ??= logger.startProgress(
536      'Waiting for a connection from $_isolateName on ${_device.name}...',
537      timeout: null, // could take an arbitrary amount of time
538    );
539    _pollingTimer ??= Timer(_pollDuration, _findIsolate);
540    return _foundUri.future.then((Uri uri) {
541      _uri = uri;
542      return uri;
543    });
544  }
545
546  Uri _uri;
547
548  void dispose() {
549    if (!_foundUri.isCompleted) {
550      _status?.cancel();
551      _status = null;
552      _pollingTimer?.cancel();
553      _pollingTimer = null;
554      _foundUri.completeError(Exception('Did not complete'));
555    }
556  }
557
558  Future<void> _findIsolate() async {
559    final List<int> ports = await _device.servicePorts();
560    for (int port in ports) {
561      VMService service;
562      if (_ports.containsKey(port)) {
563        service = _ports[port];
564      } else {
565        final int localPort = await _device.portForwarder.forward(port);
566        try {
567          final Uri uri = Uri.parse('http://[$_ipv6Loopback]:$localPort');
568          service = await _vmServiceConnector(uri);
569          _ports[port] = service;
570        } on SocketException catch (err) {
571          printTrace('Failed to connect to $localPort: $err');
572          continue;
573        }
574      }
575      await service.getVM();
576      await service.refreshViews();
577      for (FlutterView flutterView in service.vm.views) {
578        if (flutterView.uiIsolate == null) {
579          continue;
580        }
581        final Uri address = flutterView.owner.vmService.httpAddress;
582        if (flutterView.uiIsolate.name.contains(_isolateName)) {
583          _foundUri.complete(_device.ipv6
584              ? Uri.parse('http://[$_ipv6Loopback]:${address.port}/')
585              : Uri.parse('http://$_ipv4Loopback:${address.port}/'));
586          _status.stop();
587          return;
588        }
589      }
590    }
591    if (_pollOnce) {
592      _foundUri.completeError(Exception('Max iterations exceeded'));
593      _status.stop();
594      return;
595    }
596    _pollingTimer = Timer(_pollDuration, _findIsolate);
597  }
598}
599
600class _FuchsiaPortForwarder extends DevicePortForwarder {
601  _FuchsiaPortForwarder(this.device);
602
603  final FuchsiaDevice device;
604  final Map<int, Process> _processes = <int, Process>{};
605
606  @override
607  Future<int> forward(int devicePort, {int hostPort}) async {
608    hostPort ??= await os.findFreePort();
609    if (hostPort == 0) {
610      throwToolExit('Failed to forward port $devicePort. No free host-side ports');
611    }
612    // Note: the provided command works around a bug in -N, see US-515
613    // for more explanation.
614    final List<String> command = <String>[
615      'ssh',
616      '-6',
617      '-F',
618      fuchsiaArtifacts.sshConfig.absolute.path,
619      '-nNT',
620      '-vvv',
621      '-f',
622      '-L',
623      '$hostPort:$_ipv4Loopback:$devicePort',
624      device.id,
625      'true',
626    ];
627    final Process process = await processManager.start(command);
628    unawaited(process.exitCode.then((int exitCode) {
629      if (exitCode != 0) {
630        throwToolExit('Failed to forward port:$devicePort');
631      }
632    }));
633    _processes[hostPort] = process;
634    _forwardedPorts.add(ForwardedPort(hostPort, devicePort));
635    return hostPort;
636  }
637
638  @override
639  List<ForwardedPort> get forwardedPorts => _forwardedPorts;
640  final List<ForwardedPort> _forwardedPorts = <ForwardedPort>[];
641
642  @override
643  Future<void> unforward(ForwardedPort forwardedPort) async {
644    _forwardedPorts.remove(forwardedPort);
645    final Process process = _processes.remove(forwardedPort.hostPort);
646    process?.kill();
647    final List<String> command = <String>[
648      'ssh',
649      '-F',
650      fuchsiaArtifacts.sshConfig.absolute.path,
651      '-O',
652      'cancel',
653      '-vvv',
654      '-L',
655      '${forwardedPort.hostPort}:$_ipv4Loopback:${forwardedPort.devicePort}',
656      device.id
657    ];
658    final ProcessResult result = await processManager.run(command);
659    if (result.exitCode != 0) {
660      throwToolExit(result.stderr);
661    }
662  }
663}
664