fuchsia_reload.dart 18.5 KB
Newer Older
1 2 3 4 5
// Copyright 2017 The Chromium 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';
6
import 'dart:collection';
7
import 'dart:convert';
8 9 10 11

import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
12
import '../base/process_manager.dart';
13
import '../base/utils.dart';
14
import '../bundle.dart' as bundle;
15
import '../cache.dart';
16 17 18
import '../device.dart';
import '../fuchsia/fuchsia_device.dart';
import '../globals.dart';
19
import '../resident_runner.dart';
20 21
import '../run_hot.dart';
import '../runner/flutter_command.dart';
22
import '../vmservice.dart';
23 24 25 26 27 28 29

// Usage:
// With e.g. flutter_gallery already running, a HotRunner can be attached to it
// with:
// $ flutter fuchsia_reload -f ~/fuchsia -a 192.168.1.39 \
//       -g //lib/flutter/examples/flutter_gallery:flutter_gallery

30 31
final String ipv4Loopback = InternetAddress.LOOPBACK_IP_V4.address;

32 33 34 35 36 37
class FuchsiaReloadCommand extends FlutterCommand {
  FuchsiaReloadCommand() {
    addBuildModeFlags(defaultToRelease: false);
    argParser.addOption('address',
      abbr: 'a',
      help: 'Fuchsia device network name or address.');
38
    argParser.addOption('build-dir',
39
      abbr: 'b',
40 41
      defaultsTo: null,
      help: 'Fuchsia build directory, e.g. out/release-x86-64.');
42 43
    argParser.addOption('gn-target',
      abbr: 'g',
44
      help: 'GN target of the application, e.g //path/to/app:app.');
45 46 47 48
    argParser.addOption('isolate-number',
      abbr: 'i',
      help: 'To reload only one instance, specify the isolate number, e.g. '
            'the number in foo\$main-###### given by --list.');
49 50 51
    argParser.addFlag('list',
      abbr: 'l',
      defaultsTo: false,
52
      help: 'Lists the running modules. ');
53 54
    argParser.addOption('name-override',
      abbr: 'n',
55
      help: 'On-device name of the application binary.');
56 57 58 59
    argParser.addFlag('preview-dart-2',
      abbr: '2',
      defaultsTo: false,
      help: 'Preview Dart 2.0 functionality.');
60 61
    argParser.addOption('target',
      abbr: 't',
62
      defaultsTo: bundle.defaultMainPath,
63
      help: 'Target app path / main entry-point file. '
64
            'Relative to --gn-target path, e.g. lib/main.dart.');
65 66
  }

67 68 69 70 71 72
  @override
  final String name = 'fuchsia_reload';

  @override
  final String description = 'Hot reload on Fuchsia.';

73
  String _buildDir;
74 75 76
  String _projectRoot;
  String _projectName;
  String _binaryName;
77
  String _isolateNumber;
78 79 80 81 82
  String _fuchsiaProjectPath;
  String _target;
  String _address;
  String _dotPackagesPath;

83 84
  bool _list;

85 86
  @override
  Future<Null> runCommand() async {
87 88
    Cache.releaseLockEarly();

89
    await _validateArguments();
90 91

    // Find the network ports used on the device by VM service instances.
92 93
    final List<int> deviceServicePorts = await _getServicePorts();
    if (deviceServicePorts.isEmpty)
94
      throwToolExit('Couldn\'t find any running Observatory instances.');
95
    for (int port in deviceServicePorts)
96
      printTrace('Fuchsia service port: $port');
97

98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
    // Set up ssh tunnels to forward the device ports to local ports.
    final List<_PortForwarder> forwardedPorts = await _forwardPorts(
        deviceServicePorts);
    // Wrap everything in try/finally to make sure we kill the ssh processes
    // doing the port forwarding.
    try {
      final List<int> servicePorts = forwardedPorts.map(
          (_PortForwarder pf) => pf.port).toList();

      if (_list) {
        await _listVMs(servicePorts);
        // Port forwarding stops when the command ends. Keep the program running
        // until directed by the user so that Observatory URLs that we print
        // continue to work.
        printStatus('Press Enter to exit.');
        await stdin.first;
        return;
      }
116

117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
      // Check that there are running VM services on the returned
      // ports, and find the Isolates that are running the target app.
      final String isolateName = '$_binaryName\$main$_isolateNumber';
      final List<int> targetPorts = await _filterPorts(
          servicePorts, isolateName);
      if (targetPorts.isEmpty)
        throwToolExit('No VMs found running $_binaryName.');
      for (int port in targetPorts)
        printTrace('Found $_binaryName at $port');

      // Set up a device and hot runner and attach the hot runner to the first
      // vm service we found.
      final List<String> fullAddresses = targetPorts.map(
        (int p) => '$ipv4Loopback:$p'
      ).toList();
      final List<Uri> observatoryUris = fullAddresses.map(
        (String a) => Uri.parse('http://$a')
      ).toList();
135 136
      final FuchsiaDevice device = new FuchsiaDevice(
          fullAddresses[0], name: _address);
137 138 139 140 141
      final FlutterDevice flutterDevice = new FlutterDevice(
        device,
        trackWidgetCreation: false,
        previewDart2: false,
      );
142 143 144 145 146 147 148 149 150 151 152 153 154
      flutterDevice.observatoryUris = observatoryUris;
      final HotRunner hotRunner = new HotRunner(
        <FlutterDevice>[flutterDevice],
        debuggingOptions: new DebuggingOptions.enabled(getBuildInfo()),
        target: _target,
        projectRootPath: _fuchsiaProjectPath,
        packagesFilePath: _dotPackagesPath
      );
      printStatus('Connecting to $_binaryName');
      await hotRunner.attach(viewFilter: isolateName);
    } finally {
      await Future.wait(forwardedPorts.map((_PortForwarder pf) => pf.stop()));
    }
155 156
  }

157
  // A cache of VMService connections.
158
  final HashMap<int, VMService> _vmServiceCache = new HashMap<int, VMService>();
159

160
  Future<VMService> _getVMService(int port) async {
161
    if (!_vmServiceCache.containsKey(port)) {
162
      final String addr = 'http://$ipv4Loopback:$port';
163
      final Uri uri = Uri.parse(addr);
164
      final VMService vmService = await VMService.connect(uri);
165 166 167 168 169 170 171 172
      _vmServiceCache[port] = vmService;
    }
    return _vmServiceCache[port];
  }

  Future<List<FlutterView>> _getViews(List<int> ports) async {
    final List<FlutterView> views = <FlutterView>[];
    for (int port in ports) {
173
      final VMService vmService = await _getVMService(port);
174 175
      await vmService.getVM();
      await vmService.waitForViews();
176 177 178 179 180 181
      views.addAll(vmService.vm.views);
    }
    return views;
  }

  // Find ports where there is a view isolate with the given name
182
  Future<List<int>> _filterPorts(List<int> ports, String viewFilter) async {
183
    printTrace('Looing for view $viewFilter');
184 185
    final List<int> result = <int>[];
    for (FlutterView v in await _getViews(ports)) {
186 187
      if (v.uiIsolate == null)
        continue;
188 189
      final Uri addr = v.owner.vmService.httpAddress;
      printTrace('At $addr, found view: ${v.uiIsolate.name}');
190
      if (v.uiIsolate.name.contains(viewFilter))
191
        result.add(addr.port);
192 193
    }
    return result;
194 195
  }

196 197 198 199 200
  static const String _bold = '\u001B[0;1m';
  static const String _reset = '\u001B[0m';

  String _vmServiceToString(VMService vmService, {int tabDepth: 0}) {
    final Uri addr = vmService.httpAddress;
201
    final String embedder = vmService.vm.embedder;
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
    final int numIsolates = vmService.vm.isolates.length;
    final String maxRSS = getSizeAsMB(vmService.vm.maxRSS);
    final String heapSize = getSizeAsMB(vmService.vm.heapAllocatedMemoryUsage);
    int totalNewUsed = 0;
    int totalNewCap = 0;
    int totalOldUsed = 0;
    int totalOldCap = 0;
    int totalExternal = 0;
    for (Isolate i in vmService.vm.isolates) {
      totalNewUsed += i.newSpace.used;
      totalNewCap += i.newSpace.capacity;
      totalOldUsed += i.oldSpace.used;
      totalOldCap += i.oldSpace.capacity;
      totalExternal += i.newSpace.external;
      totalExternal += i.oldSpace.external;
    }
    final String newUsed = getSizeAsMB(totalNewUsed);
    final String newCap = getSizeAsMB(totalNewCap);
    final String oldUsed = getSizeAsMB(totalOldUsed);
    final String oldCap = getSizeAsMB(totalOldCap);
    final String external = getSizeAsMB(totalExternal);
    final String tabs = '\t' * tabDepth;
    final String extraTabs = '\t' * (tabDepth + 1);
    final StringBuffer stringBuffer = new StringBuffer(
226
      '$tabs$_bold$embedder at $addr$_reset\n'
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
      '${extraTabs}RSS: $maxRSS\n'
      '${extraTabs}Native allocations: $heapSize\n'
      '${extraTabs}New Spaces: $newUsed of $newCap\n'
      '${extraTabs}Old Spaces: $oldUsed of $oldCap\n'
      '${extraTabs}External: $external\n'
      '${extraTabs}Isolates: $numIsolates\n'
    );
    for (Isolate isolate in vmService.vm.isolates) {
      stringBuffer.write(_isolateToString(isolate, tabDepth: tabDepth + 1));
    }
    return stringBuffer.toString();
  }

  String _isolateToString(Isolate isolate, {int tabDepth: 0}) {
    final Uri vmServiceAddr = isolate.owner.vmService.httpAddress;
    final String name = isolate.name;
    final String shortName = name.substring(0, name.indexOf('\$'));
244
    const String main = '\$main-';
245 246 247 248
    final String number = name.substring(name.indexOf(main) + main.length);

    // The Observatory requires somewhat non-standard URIs that the Uri class
    // can't build for us, so instead we build them by hand.
249 250 251
    final String isolateIdQuery = '?isolateId=isolates%2F$number';
    final String isolateAddr = '$vmServiceAddr/#/inspect$isolateIdQuery';
    final String debuggerAddr = '$vmServiceAddr/#/debugger$isolateIdQuery';
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275

    final String newUsed = getSizeAsMB(isolate.newSpace.used);
    final String newCap = getSizeAsMB(isolate.newSpace.capacity);
    final String newFreq = '${isolate.newSpace.avgCollectionTime.inMilliseconds}ms';
    final String newPer = '${isolate.newSpace.avgCollectionPeriod.inSeconds}s';
    final String oldUsed = getSizeAsMB(isolate.oldSpace.used);
    final String oldCap = getSizeAsMB(isolate.oldSpace.capacity);
    final String oldFreq = '${isolate.oldSpace.avgCollectionTime.inMilliseconds}ms';
    final String oldPer = '${isolate.oldSpace.avgCollectionPeriod.inSeconds}s';
    final String external = getSizeAsMB(isolate.newSpace.external + isolate.oldSpace.external);
    final String tabs = '\t' * tabDepth;
    final String extraTabs = '\t' * (tabDepth + 1);
    return
      '$tabs$_bold$shortName$_reset\n'
      '${extraTabs}Isolate number: $number\n'
      '${extraTabs}Observatory: $isolateAddr\n'
      '${extraTabs}Debugger: $debuggerAddr\n'
      '${extraTabs}New gen: $newUsed used of $newCap, GC: $newFreq every $newPer\n'
      '${extraTabs}Old gen: $oldUsed used of $oldCap, GC: $oldFreq every $oldPer\n'
      '${extraTabs}External: $external\n';
  }

  Future<Null> _listVMs(List<int> ports) async {
    for (int port in ports) {
276
      final VMService vmService = await _getVMService(port);
277 278 279
      await vmService.getVM();
      await vmService.waitForViews();
      printStatus(_vmServiceToString(vmService));
280 281 282
    }
  }

283 284 285 286 287 288 289 290 291 292 293
  Future<Null> _validateArguments() async {
    _buildDir = argResults['build-dir'];
    if (_buildDir == null) {
      final ProcessResult result = await processManager.run(<String>['fx', 'get-build-dir']);
      if (result.exitCode == 0)
        _buildDir = result.stdout.trim();
      else
        printStatus('get-build-dir failed:\nstdout: ${result.stdout}\nstderr: ${result.stderr}');
    }
    if (!_directoryExists(_buildDir))
      throwToolExit('Specified --build-dir "$_buildDir" does not exist.');
294 295

    _address = argResults['address'];
296 297 298 299 300 301 302
    if (_address == null) {
      final ProcessResult result = await processManager.run(<String>['fx', 'netaddr', '--fuchsia']);
      if (result.exitCode == 0)
        _address = result.stdout.trim();
      else
        printStatus('netaddr failed:\nstdout: ${result.stdout}\nstderr: ${result.stderr}');
    }
303 304
    if (_address == null)
      throwToolExit('Give the address of the device running Fuchsia with --address.');
305

306 307 308 309 310 311
    _list = argResults['list'];
    if (_list) {
      // For --list, we only need the device address and the Fuchsia tree root.
      return;
    }

312 313 314 315 316 317
    final String gnTarget = argResults['gn-target'];
    if (gnTarget == null)
      throwToolExit('Give the GN target with --gn-target(-g).');
    final List<String> targetInfo = _extractPathAndName(gnTarget);
    _projectRoot = targetInfo[0];
    _projectName = targetInfo[1];
318
    _fuchsiaProjectPath = '$_buildDir/../../$_projectRoot';
319 320
    if (!_directoryExists(_fuchsiaProjectPath))
      throwToolExit('Target does not exist in the Fuchsia tree: $_fuchsiaProjectPath.');
321 322

    final String relativeTarget = argResults['target'];
323 324 325 326 327
    if (relativeTarget == null)
      throwToolExit('Give the application entry point with --target.');
    _target = '$_fuchsiaProjectPath/$relativeTarget';
    if (!_fileExists(_target))
      throwToolExit('Couldn\'t find application entry point at $_target.');
328

329
    final String packagesFileName = '${_projectName}_dart_library.packages';
330
    _dotPackagesPath = '$_buildDir/dartlang/gen/$_projectRoot/$packagesFileName';
331 332
    if (!_fileExists(_dotPackagesPath))
      throwToolExit('Couldn\'t find .packages file at $_dotPackagesPath.');
333 334 335 336 337 338 339

    final String nameOverride = argResults['name-override'];
    if (nameOverride == null) {
      _binaryName = _projectName;
    } else {
      _binaryName = nameOverride;
    }
340 341 342 343 344 345 346

    final String isolateNumber = argResults['isolate-number'];
    if (isolateNumber == null) {
      _isolateNumber = '';
    } else {
      _isolateNumber = '-$isolateNumber';
    }
347 348 349 350
  }

  List<String> _extractPathAndName(String gnTarget) {
    final String errorMessage =
351 352
      'fuchsia_reload --target "$gnTarget" should have the form: '
      '"//path/to/app:name"';
353 354
    // Separate strings like //path/to/target:app into [path/to/target, app]
    final int lastColon = gnTarget.lastIndexOf(':');
355
    if (lastColon < 0)
356 357 358
      throwToolExit(errorMessage);
    final String name = gnTarget.substring(lastColon + 1);
    // Skip '//' and chop off after :
359
    if ((gnTarget.length < 3) || (gnTarget[0] != '/') || (gnTarget[1] != '/'))
360 361 362 363 364
      throwToolExit(errorMessage);
    final String path = gnTarget.substring(2, lastColon);
    return <String>[path, name];
  }

365
  Future<List<_PortForwarder>> _forwardPorts(List<int> remotePorts) async {
366
    final String config = '$_buildDir/ssh-keys/ssh_config';
367 368 369 370 371 372 373
    final List<_PortForwarder> forwarders = <_PortForwarder>[];
    for (int port in remotePorts) {
      final _PortForwarder f =
          await _PortForwarder.start(config, _address, port);
      forwarders.add(f);
    }
    return forwarders;
374 375
  }

376
  Future<List<int>> _getServicePorts() async {
377
    final FuchsiaDeviceCommandRunner runner =
378
        new FuchsiaDeviceCommandRunner(_address, _buildDir);
379
    final List<String> lsOutput = await runner.run('ls /tmp/dart.services');
380
    final List<int> ports = <int>[];
381 382 383 384 385 386
    if (lsOutput != null) {
      for (String s in lsOutput) {
        final String trimmed = s.trim();
        final int lastSpace = trimmed.lastIndexOf(' ');
        final String lastWord = trimmed.substring(lastSpace + 1);
        if ((lastWord != '.') && (lastWord != '..')) {
387 388
          // ignore: deprecated_member_use
          final int value = int.parse(lastWord, onError: (_) => null);
389 390 391
          if (value != null)
            ports.add(value);
        }
392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407
      }
    }
    return ports;
  }

  bool _directoryExists(String path) {
    final Directory d = fs.directory(path);
    return d.existsSync();
  }

  bool _fileExists(String path) {
    final File f = fs.file(path);
    return f.existsSync();
  }
}

408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434
// Instances of this class represent a running ssh tunnel from the host to a
// VM service running on a Fuchsia device. [process] is the ssh process running
// the tunnel and [port] is the local port.
class _PortForwarder {
  final String _remoteAddress;
  final int _remotePort;
  final int _localPort;
  final Process _process;
  final String _sshConfig;

  _PortForwarder._(this._remoteAddress,
                   this._remotePort,
                   this._localPort,
                   this._process,
                   this._sshConfig);

  int get port => _localPort;

  static Future<_PortForwarder> start(String sshConfig,
                                      String address,
                                      int remotePort) async {
    final int localPort = await _potentiallyAvailablePort();
    if (localPort == 0) {
      printStatus(
          '_PortForwarder failed to find a local port for $address:$remotePort');
      return new _PortForwarder._(null, 0, 0, null, null);
    }
435
    const String dummyRemoteCommand = 'date';
436
    final List<String> command = <String>[
437 438
        'ssh', '-F', sshConfig, '-nNT', '-vvv', '-f',
        '-L', '$localPort:$ipv4Loopback:$remotePort', address, dummyRemoteCommand];
439 440
    printTrace("_PortForwarder running '${command.join(' ')}'");
    final Process process = await processManager.start(command);
441 442 443 444
    process.stderr
        .transform(utf8.decoder)
        .transform(const LineSplitter())
        .listen((String data) { printTrace(data); });
445 446 447 448 449 450 451 452 453 454 455 456 457 458 459
    process.exitCode.then((int c) {
      printTrace("'${command.join(' ')}' exited with exit code $c");
    });
    printTrace('Set up forwarding from $localPort to $address:$remotePort');
    return new _PortForwarder._(address, remotePort, localPort, process, sshConfig);
  }

  Future<Null> stop() async {
    // Kill the original ssh process if it is still around.
    if (_process != null) {
      printTrace('_PortForwarder killing ${_process.pid} for port $_localPort');
      _process.kill();
    }
    // Cancel the forwarding request.
    final List<String> command = <String>[
460
        'ssh', '-F', _sshConfig, '-O', 'cancel', '-vvv',
461 462 463 464
        '-L', '$_localPort:$ipv4Loopback:$_remotePort', _remoteAddress];
    final ProcessResult result = await processManager.run(command);
    printTrace(command.join(' '));
    if (result.exitCode != 0) {
465
      printTrace('Command failed:\nstdout: ${result.stdout}\nstderr: ${result.stderr}');
466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483
    }
  }

  static Future<int> _potentiallyAvailablePort() async {
    int port = 0;
    ServerSocket s;
    try {
      s = await ServerSocket.bind(ipv4Loopback, 0);
      port = s.port;
    } catch (e) {
      // Failures are signaled by a return value of 0 from this function.
      printTrace('_potentiallyAvailablePort failed: $e');
    }
    if (s != null)
      await s.close();
    return port;
  }
}
484 485

class FuchsiaDeviceCommandRunner {
486
  final String _address;
487
  final String _buildDir;
488

489
  FuchsiaDeviceCommandRunner(this._address, this._buildDir);
490 491

  Future<List<String>> run(String command) async {
492
    final String config = '$_buildDir/ssh-keys/ssh_config';
493 494 495
    final List<String> args = <String>['ssh', '-F', config, _address, command];
    printTrace(args.join(' '));
    final ProcessResult result = await processManager.run(args);
496
    if (result.exitCode != 0) {
497
      printStatus('Command failed: $command\nstdout: ${result.stdout}\nstderr: ${result.stderr}');
498 499
      return null;
    }
500 501
    printTrace(result.stdout);
    return result.stdout.split('\n');
502 503
  }
}