diff --git a/packages/flutter_tools/bin/sky_tools.dart b/packages/flutter_tools/bin/sky_tools.dart index 7df07e1f935..a0e2d71db2e 100644 --- a/packages/flutter_tools/bin/sky_tools.dart +++ b/packages/flutter_tools/bin/sky_tools.dart @@ -13,6 +13,7 @@ import 'package:sky_tools/src/build.dart'; import 'package:sky_tools/src/cache.dart'; import 'package:sky_tools/src/init.dart'; import 'package:sky_tools/src/install.dart'; +import 'package:sky_tools/src/listen.dart'; import 'package:sky_tools/src/logs.dart'; import 'package:sky_tools/src/run_mojo.dart'; import 'package:sky_tools/src/start.dart'; @@ -149,6 +150,7 @@ void main(List args) { ..addCommand(new CacheCommand()) ..addCommand(new InitCommand()) ..addCommand(new InstallCommand()) + ..addCommand(new ListenCommand()) ..addCommand(new LogsCommand()) ..addCommand(new RunMojoCommand()) ..addCommand(new StartCommand()) diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 66a02e97e25..f783b121bba 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -256,19 +256,21 @@ class AndroidDevice extends _Device { return false; } - // Set up port forwarding for observatory. - String observatoryPortString = 'tcp:$_observatoryPort'; - runCheckedSync( - [adbPath, 'forward', observatoryPortString, observatoryPortString]); + if (!poke) { + // Set up port forwarding for observatory. + String observatoryPortString = 'tcp:$_observatoryPort'; + runCheckedSync( + [adbPath, 'forward', observatoryPortString, observatoryPortString]); - // Actually start the server. - await Process.start('pub', ['run', 'sky_tools:sky_server', _serverPort], - workingDirectory: serverRoot, mode: ProcessStartMode.DETACHED); + // Actually start the server. + await Process.start('pub', ['run', 'sky_tools:sky_server', _serverPort], + workingDirectory: serverRoot, mode: ProcessStartMode.DETACHED); - // Set up reverse port-forwarding so that the Android app can reach the - // server running on localhost. - String serverPortString = 'tcp:$_serverPort'; - runCheckedSync([adbPath, 'reverse', serverPortString, serverPortString]); + // Set up reverse port-forwarding so that the Android app can reach the + // server running on localhost. + String serverPortString = 'tcp:$_serverPort'; + runCheckedSync([adbPath, 'reverse', serverPortString, serverPortString]); + } String relativeDartMain = path.relative(mainDart, from: serverRoot); String url = 'http://localhost:$_serverPort/$relativeDartMain'; diff --git a/packages/flutter_tools/lib/src/install.dart b/packages/flutter_tools/lib/src/install.dart index c9db6a22e1d..f8f45239ac0 100644 --- a/packages/flutter_tools/lib/src/install.dart +++ b/packages/flutter_tools/lib/src/install.dart @@ -15,7 +15,11 @@ class InstallCommand extends Command { final name = 'install'; final description = 'Install your Flutter app on attached devices.'; AndroidDevice android = null; - InstallCommand([this.android]); + InstallCommand([this.android]) { + if (android == null) { + android = new AndroidDevice(); + } + } @override Future run() async { @@ -31,9 +35,6 @@ class InstallCommand extends Command { Map packages = ApplicationPackageFactory.getAvailableApplicationPackages(); - if (android == null) { - android = new AndroidDevice(); - } ApplicationPackage androidApp = packages[BuildPlatform.android]; if (androidApp != null && android.isConnected()) { installedSomewhere = android.installApp(androidApp) || installedSomewhere; diff --git a/packages/flutter_tools/lib/src/listen.dart b/packages/flutter_tools/lib/src/listen.dart new file mode 100644 index 00000000000..47a38979284 --- /dev/null +++ b/packages/flutter_tools/lib/src/listen.dart @@ -0,0 +1,118 @@ +// Copyright 2015 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. + +library sky_tools.listen; + +import 'dart:async'; +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:logging/logging.dart'; +import 'package:sky_tools/src/application_package.dart'; +import 'package:sky_tools/src/device.dart'; +import 'package:sky_tools/src/process.dart'; + +final Logger _logging = new Logger('sky_tools.listen'); + +class ListenCommand extends Command { + final name = 'listen'; + final description = 'Listen for changes to files and reload the running app ' + 'on all connected devices.'; + AndroidDevice android = null; + List watchCommand; + + /// Only run once. Used for testing. + bool singleRun; + + ListenCommand({this.android, this.singleRun: false}) { + argParser.addFlag('checked', + negatable: true, + defaultsTo: true, + help: 'Toggle Dart\'s checked mode.'); + argParser.addOption('target', + defaultsTo: '.', + abbr: 't', + help: 'Target app path or filename to start.'); + + if (android == null) { + android = new AndroidDevice(); + } + } + + @override + Future run() async { + if (argResults.rest.length > 0) { + watchCommand = _initWatchCommand(argResults.rest); + } else { + watchCommand = _initWatchCommand(['.']); + } + + Map packages = + ApplicationPackageFactory.getAvailableApplicationPackages(); + ApplicationPackage androidApp = packages[BuildPlatform.android]; + + while (true) { + _logging.info('Updating running Sky apps...'); + + if (android.isConnected()) { + await android.startServer( + argResults['target'], true, argResults['checked'], androidApp); + } + + if (singleRun || !watchDirectory()) { + break; + } + } + + return 0; + } + + List _initWatchCommand(List directories) { + if (Platform.isMacOS) { + try { + runCheckedSync(['which', 'fswatch']); + } catch (e) { + _logging.severe('"listen" command is only useful if you have installed ' + 'fswatch on Mac. Run "brew install fswatch" to install it with ' + 'homebrew.'); + return null; + } + return ['fswatch', '-r', '-v', '-1']..addAll(directories); + } else if (Platform.isLinux) { + try { + runCheckedSync(['which', 'inotifywait']); + } catch (e) { + _logging.severe('"listen" command is only useful if you have installed ' + 'inotifywait on Linux. Run "apt-get install inotify-tools" or ' + 'equivalent to install it.'); + return null; + } + return [ + 'inotifywait', + '-r', + '-e', + // Only listen for events that matter, to avoid triggering constantly + // from the editor watching files + 'modify,close_write,move,create,delete', + ]..addAll(directories); + } else { + _logging.severe('"listen" command is only available on Mac and Linux.'); + } + return null; + } + + bool watchDirectory() { + if (watchCommand == null) { + return false; + } + + try { + runCheckedSync(watchCommand); + } catch (e) { + _logging.warning('Watching directories failed.', e); + return false; + } + return true; + } +} diff --git a/packages/flutter_tools/test/listen_test.dart b/packages/flutter_tools/test/listen_test.dart new file mode 100644 index 00000000000..89510860a0d --- /dev/null +++ b/packages/flutter_tools/test/listen_test.dart @@ -0,0 +1,34 @@ +// Copyright 2015 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. + +library stop_test; + +import 'package:args/command_runner.dart'; +import 'package:mockito/mockito.dart'; +import 'package:sky_tools/src/application_package.dart'; +import 'package:sky_tools/src/listen.dart'; +import 'package:test/test.dart'; + +import 'src/common.dart'; + +main() => defineTests(); + +defineTests() { + group('listen', () { + test('returns 0 when no device is connected', () { + ApplicationPackageFactory.srcPath = './'; + ApplicationPackageFactory.setBuildPath( + BuildType.prebuilt, BuildPlatform.android, './'); + + MockAndroidDevice android = new MockAndroidDevice(); + when(android.isConnected()).thenReturn(false); + ListenCommand command = + new ListenCommand(android: android, singleRun: true); + + CommandRunner runner = new CommandRunner('test_flutter', '') + ..addCommand(command); + runner.run(['listen']).then((int code) => expect(code, equals(0))); + }); + }); +}