395 lines
15 KiB
Dart
395 lines
15 KiB
Dart
// 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';
|
|
import 'dart:collection';
|
|
import 'dart:math';
|
|
|
|
import '../base/common.dart';
|
|
import '../base/file_system.dart';
|
|
import '../base/io.dart';
|
|
import '../base/platform.dart';
|
|
import '../base/utils.dart';
|
|
import '../cache.dart';
|
|
import '../device.dart';
|
|
import '../flx.dart' as flx;
|
|
import '../fuchsia/fuchsia_device.dart';
|
|
import '../globals.dart';
|
|
import '../resident_runner.dart';
|
|
import '../run_hot.dart';
|
|
import '../runner/flutter_command.dart';
|
|
import '../vmservice.dart';
|
|
|
|
// 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
|
|
|
|
class FuchsiaReloadCommand extends FlutterCommand {
|
|
FuchsiaReloadCommand() {
|
|
addBuildModeFlags(defaultToRelease: false);
|
|
argParser.addOption('address',
|
|
abbr: 'a',
|
|
help: 'Fuchsia device network name or address.');
|
|
argParser.addOption('build-type',
|
|
abbr: 'b',
|
|
defaultsTo: 'release-x86-64',
|
|
help: 'Fuchsia build type, e.g. release-x86-64.');
|
|
argParser.addOption('fuchsia-root',
|
|
abbr: 'f',
|
|
defaultsTo: platform.environment['FUCHSIA_ROOT'],
|
|
help: 'Path to Fuchsia source tree.');
|
|
argParser.addOption('gn-target',
|
|
abbr: 'g',
|
|
help: 'GN target of the application, e.g //path/to/app:app.');
|
|
argParser.addFlag('list',
|
|
abbr: 'l',
|
|
defaultsTo: false,
|
|
help: 'Lists the running modules. '
|
|
'Requires the flags --address(-a) and --fuchsia-root(-f).');
|
|
argParser.addOption('name-override',
|
|
abbr: 'n',
|
|
help: 'On-device name of the application binary.');
|
|
argParser.addOption('isolate-number',
|
|
abbr: 'i',
|
|
help: 'To reload only one instance, speficy the isolate number, e.g. '
|
|
'the number in foo\$main-###### given by --list.');
|
|
argParser.addOption('target',
|
|
abbr: 't',
|
|
defaultsTo: flx.defaultMainPath,
|
|
help: 'Target app path / main entry-point file. '
|
|
'Relative to --gn-target path, e.g. lib/main.dart.');
|
|
}
|
|
|
|
@override
|
|
final String name = 'fuchsia_reload';
|
|
|
|
@override
|
|
final String description = 'Hot reload on Fuchsia.';
|
|
|
|
String _fuchsiaRoot;
|
|
String _projectRoot;
|
|
String _projectName;
|
|
String _binaryName;
|
|
String _isolateNumber;
|
|
String _fuchsiaProjectPath;
|
|
String _target;
|
|
String _address;
|
|
String _dotPackagesPath;
|
|
|
|
bool _list;
|
|
|
|
@override
|
|
Future<Null> runCommand() async {
|
|
Cache.releaseLockEarly();
|
|
|
|
_validateArguments();
|
|
|
|
// Find the network ports used on the device by VM service instances.
|
|
final List<int> servicePorts = await _getServicePorts();
|
|
if (servicePorts.isEmpty)
|
|
throwToolExit('Couldn\'t find any running Observatory instances.');
|
|
for (int port in servicePorts)
|
|
printTrace('Fuchsia service port: $port');
|
|
|
|
if (_list) {
|
|
await _listVMs(servicePorts);
|
|
return;
|
|
}
|
|
|
|
// 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) => '$_address:$p'
|
|
).toList();
|
|
final List<Uri> observatoryUris = fullAddresses.map(
|
|
(String a) => Uri.parse('http://$a')
|
|
).toList();
|
|
final FuchsiaDevice device = new FuchsiaDevice(fullAddresses[0]);
|
|
final FlutterDevice flutterDevice = new FlutterDevice(device);
|
|
flutterDevice.observatoryUris = observatoryUris;
|
|
final HotRunner hotRunner = new HotRunner(
|
|
<FlutterDevice>[flutterDevice],
|
|
debuggingOptions: new DebuggingOptions.enabled(getBuildMode()),
|
|
target: _target,
|
|
projectRootPath: _fuchsiaProjectPath,
|
|
packagesFilePath: _dotPackagesPath
|
|
);
|
|
printStatus('Connecting to $_binaryName');
|
|
await hotRunner.attach(viewFilter: isolateName);
|
|
}
|
|
|
|
// A cache of VMService connections.
|
|
HashMap<int, VMService> _vmServiceCache = new HashMap<int, VMService>();
|
|
|
|
VMService _getVMService(int port) {
|
|
if (!_vmServiceCache.containsKey(port)) {
|
|
final String addr = 'http://$_address:$port';
|
|
final Uri uri = Uri.parse(addr);
|
|
final VMService vmService = VMService.connect(uri);
|
|
_vmServiceCache[port] = vmService;
|
|
}
|
|
return _vmServiceCache[port];
|
|
}
|
|
|
|
Future<List<FlutterView>> _getViews(List<int> ports) async {
|
|
final List<FlutterView> views = <FlutterView>[];
|
|
for (int port in ports) {
|
|
final VMService vmService = _getVMService(port);
|
|
await vmService.getVM();
|
|
await vmService.waitForViews();
|
|
views.addAll(vmService.vm.views);
|
|
}
|
|
return views;
|
|
}
|
|
|
|
// Find ports where there is a view isolate with the given name
|
|
Future<List<int>> _filterPorts(List<int> ports, String viewFilter) async {
|
|
final List<int> result = <int>[];
|
|
for (FlutterView v in await _getViews(ports)) {
|
|
final Uri addr = v.owner.vmService.httpAddress;
|
|
printTrace('At $addr, found view: ${v.uiIsolate.name}');
|
|
if (v.uiIsolate.name.indexOf(viewFilter) == 0)
|
|
result.add(addr.port);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
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;
|
|
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(
|
|
'$tabs${_bold}VM at $addr$_reset\n'
|
|
'${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('\$'));
|
|
final String main = '\$main-';
|
|
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.
|
|
final String isolateIdQuery = "?isolateId=isolates%2F$number";
|
|
final String isolateAddr = "$vmServiceAddr/#/inspect$isolateIdQuery";
|
|
final String debuggerAddr = "$vmServiceAddr/#/debugger$isolateIdQuery";
|
|
|
|
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) {
|
|
final VMService vmService = _getVMService(port);
|
|
await vmService.getVM();
|
|
await vmService.waitForViews();
|
|
printStatus(_vmServiceToString(vmService));
|
|
}
|
|
}
|
|
|
|
void _validateArguments() {
|
|
_fuchsiaRoot = argResults['fuchsia-root'];
|
|
if (_fuchsiaRoot == null)
|
|
throwToolExit('Please give the location of the Fuchsia tree with --fuchsia-root.');
|
|
if (!_directoryExists(_fuchsiaRoot))
|
|
throwToolExit('Specified --fuchsia-root "$_fuchsiaRoot" does not exist.');
|
|
|
|
_address = argResults['address'];
|
|
if (_address == null)
|
|
throwToolExit('Give the address of the device running Fuchsia with --address.');
|
|
|
|
_list = argResults['list'];
|
|
if (_list) {
|
|
// For --list, we only need the device address and the Fuchsia tree root.
|
|
return;
|
|
}
|
|
|
|
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];
|
|
_fuchsiaProjectPath = '$_fuchsiaRoot/$_projectRoot';
|
|
if (!_directoryExists(_fuchsiaProjectPath))
|
|
throwToolExit('Target does not exist in the Fuchsia tree: $_fuchsiaProjectPath.');
|
|
|
|
final String relativeTarget = argResults['target'];
|
|
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.');
|
|
|
|
final String buildType = argResults['build-type'];
|
|
if (buildType == null)
|
|
throwToolExit('Give the build type with --build-type.');
|
|
final String packagesFileName = '${_projectName}_dart_package.packages';
|
|
_dotPackagesPath = '$_fuchsiaRoot/out/$buildType/gen/$_projectRoot/$packagesFileName';
|
|
if (!_fileExists(_dotPackagesPath))
|
|
throwToolExit('Couldn\'t find .packages file at $_dotPackagesPath.');
|
|
|
|
final String nameOverride = argResults['name-override'];
|
|
if (nameOverride == null) {
|
|
_binaryName = _projectName;
|
|
} else {
|
|
_binaryName = nameOverride;
|
|
}
|
|
|
|
final String isolateNumber = argResults['isolate-number'];
|
|
if (isolateNumber == null) {
|
|
_isolateNumber = '';
|
|
} else {
|
|
_isolateNumber = '-$isolateNumber';
|
|
}
|
|
}
|
|
|
|
List<String> _extractPathAndName(String gnTarget) {
|
|
final String errorMessage =
|
|
'fuchsia_reload --target "$gnTarget" should have the form: '
|
|
'"//path/to/app:name"';
|
|
// Separate strings like //path/to/target:app into [path/to/target, app]
|
|
final int lastColon = gnTarget.lastIndexOf(':');
|
|
if (lastColon < 0)
|
|
throwToolExit(errorMessage);
|
|
final String name = gnTarget.substring(lastColon + 1);
|
|
// Skip '//' and chop off after :
|
|
if ((gnTarget.length < 3) || (gnTarget[0] != '/') || (gnTarget[1] != '/'))
|
|
throwToolExit(errorMessage);
|
|
final String path = gnTarget.substring(2, lastColon);
|
|
return <String>[path, name];
|
|
}
|
|
|
|
Future<List<int>> _getServicePorts() async {
|
|
final FuchsiaDeviceCommandRunner runner = new FuchsiaDeviceCommandRunner(_fuchsiaRoot);
|
|
final List<String> lsOutput = await runner.run('ls /tmp/dart.services');
|
|
final List<int> ports = <int>[];
|
|
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 != '..')) {
|
|
final int value = int.parse(lastWord, onError: (_) => null);
|
|
if (value != null)
|
|
ports.add(value);
|
|
}
|
|
}
|
|
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();
|
|
}
|
|
}
|
|
|
|
|
|
// TODO(zra): When Fuchsia has ssh, this should be changed to use that instead.
|
|
class FuchsiaDeviceCommandRunner {
|
|
final String _fuchsiaRoot;
|
|
final Random _rng = new Random(new DateTime.now().millisecondsSinceEpoch);
|
|
|
|
FuchsiaDeviceCommandRunner(this._fuchsiaRoot);
|
|
|
|
Future<List<String>> run(String command) async {
|
|
final int tag = _rng.nextInt(999999);
|
|
const String kNetRunCommand = 'out/build-magenta/tools/netruncmd';
|
|
final String netruncmd = fs.path.join(_fuchsiaRoot, kNetRunCommand);
|
|
const String kNetCP = 'out/build-magenta/tools/netcp';
|
|
final String netcp = fs.path.join(_fuchsiaRoot, kNetCP);
|
|
final String remoteStdout = '/tmp/netruncmd.$tag';
|
|
final String localStdout = '${fs.systemTempDirectory.path}/netruncmd.$tag';
|
|
final String redirectedCommand = '$command > $remoteStdout';
|
|
// Run the command with output directed to a tmp file.
|
|
ProcessResult result =
|
|
await Process.run(netruncmd, <String>[':', redirectedCommand]);
|
|
if (result.exitCode != 0)
|
|
return null;
|
|
// Copy that file to the local filesystem.
|
|
result = await Process.run(netcp, <String>[':$remoteStdout', localStdout]);
|
|
// Try to delete the remote file. Don't care about the result;
|
|
Process.run(netruncmd, <String>[':', 'rm $remoteStdout']);
|
|
if (result.exitCode != 0)
|
|
return null;
|
|
// Read the local file.
|
|
final File f = fs.file(localStdout);
|
|
List<String> lines;
|
|
try {
|
|
lines = await f.readAsLines();
|
|
} finally {
|
|
f.delete();
|
|
}
|
|
return lines;
|
|
}
|
|
}
|