Add --hot mode for flutter run
diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart
index 3197416..0b71755 100644
--- a/packages/flutter_tools/lib/src/android/android_device.dart
+++ b/packages/flutter_tools/lib/src/android/android_device.dart
@@ -389,6 +389,31 @@
   }
 
   @override
+  bool get supportsHotMode => true;
+
+  @override
+  Future<bool> runFromFile(ApplicationPackage package,
+                           String scriptUri,
+                           String packagesUri) async {
+    AndroidApk apk = package;
+    List<String> cmd = adbCommandForDevice(<String>[
+      'shell', 'am', 'start',
+      '-a', 'android.intent.action.RUN',
+      '-d', _deviceBundlePath,
+      '-f', '0x20000000',  // FLAG_ACTIVITY_SINGLE_TOP
+    ]);
+    cmd.addAll(<String>['--es', 'file', scriptUri]);
+    cmd.addAll(<String>['--es', 'packages', packagesUri]);
+    cmd.add(apk.launchActivity);
+    String result = runCheckedSync(cmd);
+    if (result.contains('Error: ')) {
+      printError(result.trim());
+      return false;
+    }
+    return true;
+  }
+
+  @override
   bool get supportsRestart => true;
 
   @override
diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart
index a328b5b..0486ec3 100644
--- a/packages/flutter_tools/lib/src/commands/daemon.dart
+++ b/packages/flutter_tools/lib/src/commands/daemon.dart
@@ -17,7 +17,6 @@
 import '../ios/simulators.dart';
 import '../run.dart';
 import '../runner/flutter_command.dart';
-import 'run.dart' as run;
 
 const String protocolVersion = '0.2.0';
 
@@ -292,7 +291,7 @@
     String route = _getStringArg(args, 'route');
     String mode = _getStringArg(args, 'mode');
     String target = _getStringArg(args, 'target');
-    bool reloadSources = _getBoolArg(args, 'reload-sources');
+    bool hotMode = _getBoolArg(args, 'hot');
 
     Device device = daemon.deviceDomain._getDevice(deviceId);
     if (device == null)
@@ -301,9 +300,6 @@
     if (!FileSystemEntity.isDirectorySync(projectDirectory))
       throw "'$projectDirectory' does not exist";
 
-    if (reloadSources != null)
-      run.useReloadSources = reloadSources;
-
     BuildMode buildMode = getBuildModeForName(mode) ?? BuildMode.debug;
     DebuggingOptions options;
 
@@ -327,7 +323,8 @@
       device,
       target: target,
       debuggingOptions: options,
-      usesTerminalUI: false
+      usesTerminalUI: false,
+      hotMode: hotMode
     );
 
     AppInstance app = new AppInstance(_getNextAppId(), runner);
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index 87ad569..7ff0d62 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -19,9 +19,6 @@
 import 'install.dart';
 import 'trace.dart';
 
-/// Whether the user has passed the `--reload-sources` command-line option.
-bool useReloadSources = false;
-
 abstract class RunCommandBase extends FlutterCommand {
   RunCommandBase() {
     addBuildModeFlags(defaultToRelease: false);
@@ -58,16 +55,16 @@
     argParser.addOption('debug-port',
         help: 'Listen to the given port for a debug connection (defaults to $kDefaultObservatoryPort).');
     usesPubOption();
+
     argParser.addFlag('resident',
         defaultsTo: true,
         help: 'Don\'t terminate the \'flutter run\' process after starting the application.');
 
-    // Hidden option to ship all the sources of the current project over to the
-    // embedder via the DevFS observatory API.
-    argParser.addFlag('devfs', negatable: false, hide: true);
-
-    // Send the _reloadSource command to the VM.
-    argParser.addFlag('reload-sources', negatable: true, defaultsTo: false, hide: true);
+    // Option to enable hot reloading.
+    argParser.addFlag('hot',
+                      negatable: false,
+                      defaultsTo: false,
+                      help: 'Run with support for hot reloading.');
 
     // Hidden option to enable a benchmarking mode. This will run the given
     // application, measure the startup time and the app restart time, write the
@@ -122,14 +119,25 @@
 
     Cache.releaseLockEarly();
 
-    useReloadSources = argResults['reload-sources'];
+    // Do some early error checks for hot mode.
+    bool hotMode = argResults['hot'];
+    if (hotMode) {
+      if (getBuildMode() != BuildMode.debug) {
+        printError('Hot mode only works with debug builds.');
+        return 1;
+      }
+      if (!deviceForCommand.supportsHotMode) {
+        printError('Hot mode is not supported by this device.');
+        return 1;
+      }
+    }
 
     if (argResults['resident']) {
       RunAndStayResident runner = new RunAndStayResident(
         deviceForCommand,
         target: target,
         debuggingOptions: options,
-        useDevFS: argResults['devfs']
+        hotMode: argResults['hot']
       );
 
       return runner.run(
diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart
new file mode 100644
index 0000000..09b7f8e
--- /dev/null
+++ b/packages/flutter_tools/lib/src/devfs.dart
@@ -0,0 +1,237 @@
+// Copyright 2016 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:convert' show BASE64, UTF8;
+import 'dart:io';
+
+import 'package:path/path.dart' as path;
+
+import 'dart/package_map.dart';
+import 'globals.dart';
+import 'observatory.dart';
+
+// A file that has been added to a DevFS.
+class DevFSEntry {
+  DevFSEntry(this.devicePath, this.file);
+
+  final String devicePath;
+  final File file;
+  FileStat _fileStat;
+
+  DateTime get lastModified => _fileStat?.modified;
+  bool get stillExists {
+    _stat();
+    return _fileStat.type != FileSystemEntityType.NOT_FOUND;
+  }
+  bool get isModified {
+    if (_fileStat == null) {
+      _stat();
+      return true;
+    }
+    FileStat _oldFileStat = _fileStat;
+    _stat();
+    return _fileStat.modified.isAfter(_oldFileStat.modified);
+  }
+
+  void _stat() {
+    _fileStat = file.statSync();
+  }
+}
+
+
+/// Abstract DevFS operations interface.
+abstract class DevFSOperations {
+  Future<Uri> create(String fsName);
+  Future<dynamic> destroy(String fsName);
+  Future<dynamic> writeFile(String fsName, DevFSEntry entry);
+  Future<dynamic> writeSource(String fsName,
+                              String devicePath,
+                              String contents);
+}
+
+/// An implementation of [DevFSOperations] that speaks to the
+/// service protocol.
+class ServiceProtocolDevFSOperations implements DevFSOperations {
+  final Observatory  serviceProtocol;
+
+  ServiceProtocolDevFSOperations(this.serviceProtocol);
+
+  @override
+  Future<Uri> create(String fsName) async {
+    Response response = await serviceProtocol.createDevFS(fsName);
+    return Uri.parse(response['uri']);
+  }
+
+  @override
+  Future<dynamic> destroy(String fsName) async {
+    await serviceProtocol.sendRequest('_deleteDevFS',
+                                      <String, dynamic> { 'fsName': fsName });
+  }
+
+  @override
+  Future<dynamic> writeFile(String fsName, DevFSEntry entry) async {
+    List<int> bytes;
+    try {
+      bytes = await entry.file.readAsBytes();
+    } catch (e) {
+      return e;
+    }
+    String fileContents = BASE64.encode(bytes);
+    return await serviceProtocol.sendRequest('_writeDevFSFile',
+                                             <String, dynamic> {
+                                                'fsName': fsName,
+                                                'path': entry.devicePath,
+                                                'fileContents': fileContents
+                                             });
+  }
+
+  @override
+  Future<dynamic> writeSource(String fsName,
+                              String devicePath,
+                              String contents) async {
+    String fileContents = BASE64.encode(UTF8.encode(contents));
+    return await serviceProtocol.sendRequest('_writeDevFSFile',
+                                             <String, dynamic> {
+                                                'fsName': fsName,
+                                                'path': devicePath,
+                                                'fileContents': fileContents
+                                             });
+  }
+}
+
+class DevFS {
+  /// Create a [DevFS] named [fsName] for the local files in [directory].
+  DevFS(Observatory serviceProtocol,
+        this.fsName,
+        this.rootDirectory)
+    : _operations = new ServiceProtocolDevFSOperations(serviceProtocol);
+
+  DevFS.operations(this._operations,
+                   this.fsName,
+                   this.rootDirectory);
+
+  final DevFSOperations _operations;
+  final String fsName;
+  final Directory rootDirectory;
+  final Map<String, DevFSEntry> _entries = <String, DevFSEntry>{};
+  final List<Future<Response>> _pendingWrites = new List<Future<Response>>();
+  Uri _baseUri;
+  Uri get baseUri => _baseUri;
+
+  Future<Uri> create() async {
+    _baseUri = await _operations.create(fsName);
+    printTrace('DevFS: Created new filesystem on the device ($_baseUri)');
+    return _baseUri;
+  }
+
+  Future<dynamic> destroy() async {
+    printTrace('DevFS: Deleted filesystem on the device ($_baseUri)');
+    return await _operations.destroy(fsName);
+  }
+
+  Future<dynamic> update() async {
+    printTrace('DevFS: Starting sync from $rootDirectory');
+    // Send the root and lib directories.
+    Directory directory = rootDirectory;
+    _syncDirectory(directory, recursive: true);
+    String packagesFilePath = path.join(rootDirectory.path, kPackagesFileName);
+    StringBuffer sb;
+    // Send the packages.
+    if (FileSystemEntity.isFileSync(packagesFilePath)) {
+      PackageMap packageMap = new PackageMap(kPackagesFileName);
+
+      for (String packageName in packageMap.map.keys) {
+        Uri uri = packageMap.map[packageName];
+        // Ignore self-references.
+        if (uri.toString() == 'lib/')
+          continue;
+        Directory directory = new Directory.fromUri(uri);
+        if (_syncDirectory(directory,
+                           directoryName: 'packages/$packageName',
+                           recursive: true)) {
+          if (sb == null) {
+            sb = new StringBuffer();
+          }
+          sb.writeln('$packageName:packages/$packageName');
+        }
+      }
+    }
+    printTrace('DevFS: Waiting for sync of ${_pendingWrites.length} files '
+               'to finish');
+    await Future.wait(_pendingWrites);
+    _pendingWrites.clear();
+    if (sb != null) {
+      await _operations.writeSource(fsName, '.packages', sb.toString());
+    }
+    printTrace('DevFS: Sync finished');
+    // NB: You must call flush after a printTrace if you want to be printed
+    // immediately.
+    logger.flush();
+  }
+
+  void _syncFile(String devicePath, File file) {
+    DevFSEntry entry = _entries[devicePath];
+    if (entry == null) {
+      // New file.
+      entry = new DevFSEntry(devicePath, file);
+      _entries[devicePath] = entry;
+    }
+    bool needsWrite = entry.isModified;
+    if (needsWrite) {
+      Future<dynamic> pendingWrite = _operations.writeFile(fsName, entry);
+      if (pendingWrite != null) {
+        _pendingWrites.add(pendingWrite);
+      } else {
+        printTrace('DevFS: Failed to sync "$devicePath"');
+      }
+    }
+  }
+
+  bool _shouldIgnore(String path) {
+    List<String> ignoredPrefixes = <String>['android/',
+                                            'build/',
+                                            'ios/',
+                                            'packages/analyzer'];
+    for (String ignoredPrefix in ignoredPrefixes) {
+      if (path.startsWith(ignoredPrefix))
+        return true;
+    }
+    return false;
+  }
+
+  bool _syncDirectory(Directory directory,
+                      {String directoryName,
+                       bool recursive: false,
+                       bool ignoreDotFiles: true}) {
+    String prefix = directoryName;
+    if (prefix == null) {
+      prefix = path.relative(directory.path, from: rootDirectory.path);
+      if (prefix == '.')
+        prefix = '';
+    }
+    try {
+      List<FileSystemEntity> files =
+          directory.listSync(recursive: recursive, followLinks: false);
+      for (FileSystemEntity file in files) {
+        if (file is! File) {
+          // Skip non-files.
+          continue;
+        }
+        if (ignoreDotFiles && path.basename(file.path).startsWith('.')) {
+          // Skip dot files.
+          continue;
+        }
+        final String devicePath =
+            path.join(prefix, path.relative(file.path, from: directory.path));
+        if (!_shouldIgnore(devicePath))
+          _syncFile(devicePath, file);
+      }
+    } catch (e) {
+      // Ignore directory and error.
+      return false;
+    }
+    return true;
+  }
+}
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index 6786712..d343e5f 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -189,6 +189,19 @@
     Map<String, dynamic> platformArgs
   });
 
+  /// Does this device implement support for hot reloading / restarting?
+  bool get supportsHotMode => false;
+
+  /// Does this device need a DevFS to support hot mode?
+  bool get needsDevFS => true;
+
+  /// Run from a file. Necessary for hot mode.
+  Future<bool> runFromFile(ApplicationPackage package,
+                           String scriptUri,
+                           String packagesUri) {
+    throw 'runFromFile unsupported';
+  }
+
   bool get supportsRestart => false;
 
   bool get restartSendsFrameworkInitEvent => true;
diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart
index 4bc2eb9..78cd7d9 100644
--- a/packages/flutter_tools/lib/src/ios/simulators.dart
+++ b/packages/flutter_tools/lib/src/ios/simulators.dart
@@ -13,11 +13,9 @@
 import '../base/context.dart';
 import '../base/process.dart';
 import '../build_info.dart';
-import '../commands/run.dart' as run;
 import '../device.dart';
 import '../flx.dart' as flx;
 import '../globals.dart';
-import '../observatory.dart';
 import '../protocol_discovery.dart';
 import 'mac.dart';
 
@@ -361,6 +359,12 @@
   @override
   bool get isLocalEmulator => true;
 
+  @override
+  bool get supportsHotMode => true;
+
+  @override
+  bool get needsDevFS => false;
+
   _IOSSimulatorLogReader _logReader;
   _IOSSimulatorDevicePortForwarder _portForwarder;
 
@@ -576,32 +580,6 @@
   }
 
   @override
-  bool get supportsRestart => run.useReloadSources;
-
-  @override
-  bool get restartSendsFrameworkInitEvent => false;
-
-  @override
-  Future<bool> restartApp(
-    ApplicationPackage package,
-    LaunchResult result, {
-    String mainPath,
-    Observatory observatory
-  }) async {
-    if (observatory.firstIsolateId == null)
-      throw 'Application isolate not found';
-    Event result = await observatory.reloadSources(observatory.firstIsolateId);
-    dynamic error = result.response['reloadError'];
-    if (error != null) {
-      printError('Error reloading application sources: $error');
-      return false;
-    } else {
-      await observatory.flutterReassemble(observatory.firstIsolateId);
-      return true;
-    }
-  }
-
-  @override
   Future<bool> stopApp(ApplicationPackage app) async {
     // Currently we don't have a way to stop an app running on iOS.
     return false;
diff --git a/packages/flutter_tools/lib/src/observatory.dart b/packages/flutter_tools/lib/src/observatory.dart
index b9d23be..bc6a315 100644
--- a/packages/flutter_tools/lib/src/observatory.dart
+++ b/packages/flutter_tools/lib/src/observatory.dart
@@ -9,6 +9,7 @@
 import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
 import 'package:web_socket_channel/io.dart';
 
+// TODO(johnmccutchan): Rename this class to ServiceProtocol or VmService.
 class Observatory {
   Observatory._(this.peer, this.port) {
     peer.registerMethod('streamNotify', (rpc.Parameters event) {
@@ -169,13 +170,13 @@
     });
   }
 
-  // Write multiple files into a file system.
-  Future<Response> writeDevFSFiles(String fsName, { List<DevFSFile> files }) {
-    assert(files != null);
-
-    return sendRequest('_writeDevFSFiles', <String, dynamic> {
+  // Read one file from a file system.
+  Future<List<int>> readDevFSFile(String fsName, String path) {
+    return sendRequest('_readDevFSFile', <String, dynamic> {
       'fsName': fsName,
-      'files': files.map((DevFSFile file) => file.toJson()).toList()
+      'path': path
+    }).then((Response response) {
+      return BASE64.decode(response.response['fileContents']);
     });
   }
 
@@ -233,25 +234,6 @@
   }
 }
 
-abstract class DevFSFile {
-  DevFSFile(this.path);
-
-  final String path;
-
-  List<int> getContents();
-
-  List<String> toJson() => <String>[path, BASE64.encode(getContents())];
-}
-
-class ByteDevFSFile extends DevFSFile {
-  ByteDevFSFile(String path, this.contents): super(path);
-
-  final List<int> contents;
-
-  @override
-  List<int> getContents() => contents;
-}
-
 class Response {
   Response(this.response);
 
diff --git a/packages/flutter_tools/lib/src/run.dart b/packages/flutter_tools/lib/src/run.dart
index c683772..43a357f 100644
--- a/packages/flutter_tools/lib/src/run.dart
+++ b/packages/flutter_tools/lib/src/run.dart
@@ -14,10 +14,10 @@
 import 'commands/build_apk.dart';
 import 'commands/install.dart';
 import 'commands/trace.dart';
-import 'dart/package_map.dart';
 import 'device.dart';
 import 'globals.dart';
 import 'observatory.dart';
+import 'devfs.dart';
 
 /// Given the value of the --target option, return the path of the Dart file
 /// where the app's main function should be.
@@ -37,20 +37,20 @@
     this.target,
     this.debuggingOptions,
     this.usesTerminalUI: true,
-    this.useDevFS: false
+    this.hotMode: false
   });
 
   final Device device;
   final String target;
   final DebuggingOptions debuggingOptions;
   final bool usesTerminalUI;
-  final bool useDevFS;
+  final bool hotMode;
 
   ApplicationPackage _package;
   String _mainPath;
   LaunchResult _result;
 
-  Completer<int> _exitCompleter = new Completer<int>();
+  final Completer<int> _exitCompleter = new Completer<int>();
   StreamSubscription<String> _loggingSubscription;
 
   Observatory observatory;
@@ -207,7 +207,15 @@
     if (debuggingOptions.debuggingEnabled) {
       observatory = await Observatory.connect(_result.observatoryPort);
       printTrace('Connected to observatory port: ${_result.observatoryPort}.');
-
+      if (hotMode && device.needsDevFS) {
+        bool result = await _updateDevFS();
+        if (!result) {
+          printError('Could not perform initial file synchronization.');
+          return 3;
+        }
+        printStatus('Launching from sources.');
+        await _launchFromDevFS(_package, _mainPath);
+      }
       observatory.populateIsolateInfo();
       observatory.onExtensionEvent.listen((Event event) {
         printTrace(event.toString());
@@ -250,15 +258,17 @@
             // F1, help
             _printHelp();
           } else if (lower == 'r' || code == AnsiTerminal.KEY_F5) {
-            if (device.supportsRestart) {
-              // F5, restart
-              restart();
+            if (hotMode) {
+              _reloadSources();
+            } else {
+              if (device.supportsRestart) {
+                // F5, restart
+                restart();
+              }
             }
           } else if (lower == 'q' || code == AnsiTerminal.KEY_F10) {
             // F10, exit
             _stopApp();
-          } else if (useDevFS && lower == 'd') {
-            _updateDevFS();
           } else if (lower == 'w') {
             _debugDumpApp();
           } else if (lower == 't') {
@@ -269,12 +279,14 @@
 
       ProcessSignal.SIGINT.watch().listen((ProcessSignal signal) async {
         _resetTerminal();
+        await _cleanupDevFS();
         await _stopLogger();
         await _stopApp();
         exit(0);
       });
       ProcessSignal.SIGTERM.watch().listen((ProcessSignal signal) async {
         _resetTerminal();
+        await _cleanupDevFS();
         await _stopLogger();
         await _stopApp();
         exit(0);
@@ -311,78 +323,84 @@
     observatory.flutterDebugDumpRenderTree(observatory.firstIsolateId);
   }
 
-  DevFS devFS;
-
-  Future<Null> _updateDevFS() async {
-    if (devFS == null) {
-      devFS = new DevFS(Directory.current, observatory);
+  DevFS _devFS;
+  String _devFSProjectRootPath;
+  Future<bool> _updateDevFS() async {
+    if (_devFS == null) {
+      Directory directory = Directory.current;
+      _devFSProjectRootPath = directory.path;
+      String fsName = path.basename(directory.path);
+      _devFS = new DevFS(observatory, fsName, directory);
 
       try {
-        await devFS.init();
+        await _devFS.create();
       } catch (error) {
-        devFS = null;
-        printError('Error initializing development client: $error');
-        return null;
+        _devFS = null;
+        printError('Error initializing DevFS: $error');
+        return false;
       }
+
+      _exitCompleter.future.then((_) async {
+        await _cleanupDevFS();
+      });
     }
 
-    // Send the root and lib directories.
-    Directory directory = Directory.current;
-    _sendFiles(directory, '', _dartFiles(directory.listSync()));
-
-    directory = new Directory('lib');
-    _sendFiles(directory, 'lib', _dartFiles(directory.listSync(recursive: true)));
-
-    // Send the packages.
-    if (FileSystemEntity.isFileSync(kPackagesFileName)) {
-      PackageMap packageMap = new PackageMap(kPackagesFileName);
-
-      for (String packageName in packageMap.map.keys) {
-        Uri uri = packageMap.map[packageName];
-        // Ignore self-references.
-        if (uri.toString() == 'lib/')
-          continue;
-        Directory directory = new Directory.fromUri(uri);
-        if (directory.existsSync()) {
-          _sendFiles(
-            directory,
-            'packages/$packageName',
-            _dartFiles(directory.listSync(recursive: true))
-          );
-        }
-      }
-    }
-
-    try {
-      await devFS.flush();
-    } catch (error) {
-      printError('Error sending sources to the client device: $error');
-    }
+    printStatus('DevFS: Updating files on device...');
+    await _devFS.update();
+    printStatus('DevFS: Finished updating files on device...');
+    return true;
   }
 
-  void _sendFiles(Directory base, String prefix, List<File> files) {
-    String basePath = base.path;
-
-    for (File file in files) {
-      String devPath = file.path.substring(basePath.length);
-      if (devPath.startsWith('/'))
-        devPath = devPath.substring(1);
-      devFS.stageFile(prefix.isEmpty ? devPath : '$prefix/$devPath', file);
+  Future<Null> _cleanupDevFS() async {
+    if (_devFS != null) {
+      // Cleanup the devFS.
+      await _devFS.destroy();
     }
+    _devFS = null;
   }
 
-  List<File> _dartFiles(List<FileSystemEntity> entities) {
-    return new List<File>.from(entities
-      .where((FileSystemEntity entity) => entity is File && entity.path.endsWith('.dart')));
+  Future<Null> _launchFromDevFS(ApplicationPackage package,
+                                String mainScript) async {
+    String entryPath = path.relative(mainScript, from: _devFSProjectRootPath);
+    String deviceEntryPath =
+        _devFS.baseUri.resolve(entryPath).toFilePath();
+    String devicePackagesPath =
+        _devFS.baseUri.resolve('.packages').toFilePath();
+    await device.runFromFile(package,
+                             deviceEntryPath,
+                             devicePackagesPath);
+  }
+
+  Future<bool> _reloadSources() async {
+    if (observatory.firstIsolateId == null)
+      throw 'Application isolate not found';
+    if (_devFS != null) {
+      await _updateDevFS();
+    }
+    Status reloadStatus = logger.startProgress('Performing hot reload');
+    Event result = await observatory.reloadSources(observatory.firstIsolateId);
+    reloadStatus.stop(showElapsedTime: true);
+    dynamic error = result.response['reloadError'];
+    if (error != null) {
+      printError('Error reloading application sources: $error');
+      return false;
+    }
+    Status reassembleStatus =
+        logger.startProgress('Reassembling application');
+    await observatory.flutterReassemble(observatory.firstIsolateId);
+    reassembleStatus.stop(showElapsedTime: true);
+    return true;
   }
 
   void _printHelp() {
-    String restartText = device.supportsRestart ? ', "r" or F5 to restart the app,' : '';
+    String restartText = '';
+    if (hotMode) {
+      restartText = ', "r" or F5 to perform a hot reload of the app,';
+    } else if (device.supportsRestart) {
+      restartText = ', "r" or F5 to restart the app,';
+    }
     printStatus('Type "h" or F1 for help$restartText and "q", F10, or ctrl-c to quit.');
     printStatus('Type "w" to print the widget hierarchy of the app, and "t" for the render tree.');
-
-    if (useDevFS)
-      printStatus('Type "d" to send modified project files to the the client\'s DevFS.');
   }
 
   Future<dynamic> _stopLogger() {
@@ -433,88 +451,3 @@
   new File(benchmarkOut).writeAsStringSync(toPrettyJson(data));
   printStatus('Run benchmark written to $benchmarkOut ($data).');
 }
-
-class DevFS {
-  DevFS(this.directory, this.observatory) {
-    fsName = path.basename(directory.path);
-  }
-
-  final Directory directory;
-  final Observatory observatory;
-
-  String fsName;
-  String uri;
-  Map<String, _DevFSFileEntry> entries = <String, _DevFSFileEntry>{};
-
-  Future<Null> init() async {
-    CreateDevFSResponse response = await observatory.createDevFS(fsName);
-    uri = response.uri;
-  }
-
-  void stageFile(String devPath, File file) {
-    entries.putIfAbsent(devPath, () => new _DevFSFileEntry(devPath, file));
-  }
-
-  /// Flush any modified files to the devfs.
-  Future<Null> flush() async {
-    List<_DevFSFileEntry> toSend = entries.values
-      .where((_DevFSFileEntry entry) => entry.isModified)
-      .toList();
-
-    for (_DevFSFileEntry entry in toSend) {
-      printTrace('sending to devfs: ${entry.devPath}');
-      entry.updateLastModified();
-    }
-
-    Status status = logger.startProgress('Sending ${toSend.length} files...');
-
-    if (toSend.isEmpty) {
-      status.stop(showElapsedTime: true);
-      return;
-    }
-
-    try {
-      List<_DevFSFile> files = toSend.map((_DevFSFileEntry entry) {
-        return new _DevFSFile('/${entry.devPath}', entry.file);
-      }).toList();
-
-      // TODO(devoncarew): Batch this up in larger groups using writeDevFSFiles().
-      // The current implementation leaves dangling service protocol calls on a timeout.
-      await Future.wait(files.map((_DevFSFile file) {
-        return observatory.writeDevFSFile(
-          fsName,
-          path: file.path,
-          fileContents: file.getContents()
-        );
-      })).timeout(new Duration(seconds: 10));
-    } finally {
-      status.stop(showElapsedTime: true);
-    }
-  }
-
-  Future<List<String>> listDevFSFiles() => observatory.listDevFSFiles(fsName);
-}
-
-class _DevFSFileEntry {
-  _DevFSFileEntry(this.devPath, this.file);
-
-  final String devPath;
-  final File file;
-
-  DateTime lastModified;
-
-  bool get isModified => lastModified == null || file.lastModifiedSync().isAfter(lastModified);
-
-  void updateLastModified() {
-    lastModified = file.lastModifiedSync();
-  }
-}
-
-class _DevFSFile extends DevFSFile {
-  _DevFSFile(String path, this.file) : super(path);
-
-  final File file;
-
-  @override
-  List<int> getContents() => file.readAsBytesSync();
-}
diff --git a/packages/flutter_tools/test/devfs_test.dart b/packages/flutter_tools/test/devfs_test.dart
new file mode 100644
index 0000000..3f8f18b
--- /dev/null
+++ b/packages/flutter_tools/test/devfs_test.dart
@@ -0,0 +1,59 @@
+// Copyright 2016 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:io';
+
+import 'package:flutter_tools/src/devfs.dart';
+import 'package:path/path.dart' as path;
+import 'package:test/test.dart';
+
+import 'src/context.dart';
+import 'src/mocks.dart';
+
+void main() {
+  String filePath = 'bar/foo.txt';
+  String filePath2 = 'foo/bar.txt';
+  Directory tempDir;
+  String basePath;
+  MockDevFSOperations devFSOperations = new MockDevFSOperations();
+  DevFS devFS;
+  group('devfs', () {
+    testUsingContext('create local file system', () async {
+      tempDir = Directory.systemTemp.createTempSync();
+      basePath = tempDir.path;
+      File file = new File(path.join(basePath, filePath));
+      await file.parent.create(recursive: true);
+      file.writeAsBytesSync(<int>[1, 2, 3]);
+    });
+    testUsingContext('create dev file system', () async {
+      devFS = new DevFS.operations(devFSOperations, 'test', tempDir);
+      await devFS.create();
+      expect(devFSOperations.contains('create test'), isTrue);
+    });
+    testUsingContext('populate dev file system', () async {
+      await devFS.update();
+      expect(devFSOperations.contains('writeFile test bar/foo.txt'), isTrue);
+    });
+    testUsingContext('modify existing file on local file system', () async {
+      File file = new File(path.join(basePath, filePath));
+      file.writeAsBytesSync(<int>[1, 2, 3, 4, 5, 6]);
+    });
+    testUsingContext('update dev file system', () async {
+      await devFS.update();
+      expect(devFSOperations.contains('writeFile test bar/foo.txt'), isTrue);
+    });
+    testUsingContext('add new file to local file system', () async {
+      File file = new File(path.join(basePath, filePath2));
+      await file.parent.create(recursive: true);
+      file.writeAsBytesSync(<int>[1, 2, 3, 4, 5, 6, 7]);
+    });
+    testUsingContext('update dev file system', () async {
+      await devFS.update();
+      expect(devFSOperations.contains('writeFile test foo/bar.txt'), isTrue);
+    });
+    testUsingContext('delete dev file system', () async {
+      await devFS.destroy();
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart
index aeccdb5..49706e7 100644
--- a/packages/flutter_tools/test/src/mocks.dart
+++ b/packages/flutter_tools/test/src/mocks.dart
@@ -7,6 +7,7 @@
 import 'package:flutter_tools/src/android/android_device.dart';
 import 'package:flutter_tools/src/application_package.dart';
 import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/devfs.dart';
 import 'package:flutter_tools/src/device.dart';
 import 'package:flutter_tools/src/ios/devices.dart';
 import 'package:flutter_tools/src/ios/simulators.dart';
@@ -69,3 +70,36 @@
     ..applicationPackages = new MockApplicationPackageStore()
     ..commandValidator = () => true;
 }
+
+class MockDevFSOperations implements DevFSOperations {
+  final List<String> messages = new List<String>();
+
+  bool contains(String match) {
+    bool result = messages.contains(match);
+    messages.clear();
+    return result;
+  }
+
+  @override
+  Future<Uri> create(String fsName) async {
+    messages.add('create $fsName');
+    return Uri.parse('file:///$fsName');
+  }
+
+  @override
+  Future<dynamic> destroy(String fsName) async {
+    messages.add('destroy $fsName');
+  }
+
+  @override
+  Future<dynamic> writeFile(String fsName, DevFSEntry entry) async {
+    messages.add('writeFile $fsName ${entry.devicePath}');
+  }
+
+  @override
+  Future<dynamic> writeSource(String fsName,
+                              String devicePath,
+                              String contents) async {
+    messages.add('writeSource $fsName $devicePath');
+  }
+}