Revert "Devfs cleanup and testing (#33374)" (#33673)

This reverts commit 445505d6f2d4a0b658385f0ef735d3a826ca49ab.
diff --git a/packages/flutter_tools/bin/fuchsia_asset_builder.dart b/packages/flutter_tools/bin/fuchsia_asset_builder.dart
index 0163a1b..2de5624 100644
--- a/packages/flutter_tools/bin/fuchsia_asset_builder.dart
+++ b/packages/flutter_tools/bin/fuchsia_asset_builder.dart
@@ -36,9 +36,10 @@
   });
 }
 
-void writeFile(libfs.File outputFile, DevFSContent content) {
+Future<void> writeFile(libfs.File outputFile, DevFSContent content) async {
   outputFile.createSync(recursive: true);
-  content.copyToFile(outputFile);
+  final List<int> data = await content.contentsAsBytes();
+  outputFile.writeAsBytesSync(data);
 }
 
 Future<void> run(List<String> args) async {
@@ -70,10 +71,12 @@
     exit(1);
   }
 
+  final List<Future<void>> calls = <Future<void>>[];
   assets.entries.forEach((String fileName, DevFSContent content) {
     final libfs.File outputFile = libfs.fs.file(libfs.fs.path.join(assetDir, fileName));
-    writeFile(outputFile, content);
+    calls.add(writeFile(outputFile, content));
   });
+  await Future.wait<void>(calls);
 
   final String outputMan = argResults[_kOptionAssetManifestOut];
   await writeFuchsiaManifest(assets, argResults[_kOptionAsset], outputMan, argResults[_kOptionComponentName]);
diff --git a/packages/flutter_tools/lib/src/bundle.dart b/packages/flutter_tools/lib/src/bundle.dart
index 3355f72..70a4265 100644
--- a/packages/flutter_tools/lib/src/bundle.dart
+++ b/packages/flutter_tools/lib/src/bundle.dart
@@ -147,7 +147,7 @@
   if (assets == null)
     throwToolExit('Error building assets', exitCode: 1);
 
-  assemble(
+  await assemble(
     buildMode: buildMode,
     assetBundle: assets,
     kernelContent: kernelContent,
@@ -182,14 +182,14 @@
   return assetBundle;
 }
 
-void assemble({
+Future<void> assemble({
   BuildMode buildMode,
   AssetBundle assetBundle,
   DevFSContent kernelContent,
   String privateKeyPath = defaultPrivateKeyPath,
   String assetDirPath,
   String compilationTraceFilePath,
-}) {
+}) async {
   assetDirPath ??= getAssetBuildDirectory();
   printTrace('Building bundle');
 
@@ -214,21 +214,22 @@
   printTrace('Writing asset files to $assetDirPath');
   ensureDirectoryExists(assetDirPath);
 
-  writeBundle(fs.directory(assetDirPath), assetEntries);
+  await writeBundle(fs.directory(assetDirPath), assetEntries);
   printTrace('Wrote $assetDirPath');
 }
 
-void writeBundle(
+Future<void> writeBundle(
   Directory bundleDir,
   Map<String, DevFSContent> assetEntries,
-) {
+) async {
   if (bundleDir.existsSync())
     bundleDir.deleteSync(recursive: true);
   bundleDir.createSync(recursive: true);
 
-  for (MapEntry<String, DevFSContent> entry in assetEntries.entries) {
-    final File file = fs.file(fs.path.join(bundleDir.path, entry.key));
-    file.parent.createSync(recursive: true);
-    entry.value.copyToFile(file);
-  }
+  await Future.wait<void>(
+    assetEntries.entries.map<Future<void>>((MapEntry<String, DevFSContent> entry) async {
+      final File file = fs.file(fs.path.join(bundleDir.path, entry.key));
+      file.parent.createSync(recursive: true);
+      await file.writeAsBytes(await entry.value.contentsAsBytes());
+    }));
 }
diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart
index 6a2307f..6f1caa4 100644
--- a/packages/flutter_tools/lib/src/commands/test.dart
+++ b/packages/flutter_tools/lib/src/commands/test.dart
@@ -242,7 +242,7 @@
       throwToolExit('Error: Failed to build asset bundle');
     }
     if (_needRebuild(assetBundle.entries)) {
-      writeBundle(fs.directory(fs.path.join('build', 'unit_test_assets')),
+      await writeBundle(fs.directory(fs.path.join('build', 'unit_test_assets')),
           assetBundle.entries);
     }
   }
diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart
index ec97f95..e15fb5c 100644
--- a/packages/flutter_tools/lib/src/devfs.dart
+++ b/packages/flutter_tools/lib/src/devfs.dart
@@ -38,25 +38,21 @@
   /// or if the given time is null.
   bool isModifiedAfter(DateTime time);
 
-  /// The number of bytes in this file.
   int get size;
 
-  /// Returns the raw bytes of this file.
-  List<int> contentsAsBytes();
+  Future<List<int>> contentsAsBytes();
 
-  /// Returns a gzipped representation of the contents of this file.
-  List<int> contentsAsCompressedBytes() {
-    return gzip.encode(contentsAsBytes());
+  Stream<List<int>> contentsAsStream();
+
+  Stream<List<int>> contentsAsCompressedStream() {
+    return contentsAsStream().cast<List<int>>().transform<List<int>>(gzip.encoder);
   }
 
-  /// Copies the content into the provided file.
-  ///
-  /// Requires that the `destination` directory already exists, but the target
-  /// file need not.
-  void copyToFile(File destination);
+  /// Return the list of files this content depends on.
+  List<String> get fileDependencies => <String>[];
 }
 
-/// File content to be copied to the device.
+// File content to be copied to the device.
 class DevFSFileContent extends DevFSContent {
   DevFSFileContent(this.file);
 
@@ -107,6 +103,9 @@
   }
 
   @override
+  List<String> get fileDependencies => <String>[_getFile().path];
+
+  @override
   bool get isModified {
     final FileStat _oldFileStat = _fileStat;
     _stat();
@@ -136,12 +135,10 @@
   }
 
   @override
-  List<int> contentsAsBytes() => _getFile().readAsBytesSync().cast<int>();
+  Future<List<int>> contentsAsBytes() => _getFile().readAsBytes();
 
   @override
-  void copyToFile(File destination) {
-    _getFile().copySync(destination.path);
-  }
+  Stream<List<int>> contentsAsStream() => _getFile().openRead();
 }
 
 /// Byte content to be copied to the device.
@@ -178,15 +175,14 @@
   int get size => _bytes.length;
 
   @override
-  List<int> contentsAsBytes() => _bytes;
+  Future<List<int>> contentsAsBytes() async => _bytes;
 
   @override
-  void copyToFile(File destination) {
-    destination.writeAsBytesSync(contentsAsBytes());
-  }
+  Stream<List<int>> contentsAsStream() =>
+      Stream<List<int>>.fromIterable(<List<int>>[_bytes]);
 }
 
-/// String content to be copied to the device encoded as utf8.
+/// String content to be copied to the device.
 class DevFSStringContent extends DevFSByteContent {
   DevFSStringContent(String string)
     : _string = string,
@@ -207,29 +203,75 @@
   }
 }
 
-class DevFSOperations {
-  DevFSOperations(this.vmService, this.fsName)
-      : httpAddress = vmService.httpAddress;
+/// Abstract DevFS operations interface.
+abstract class DevFSOperations {
+  Future<Uri> create(String fsName);
+  Future<dynamic> destroy(String fsName);
+  Future<dynamic> writeFile(String fsName, Uri deviceUri, DevFSContent content);
+}
+
+/// An implementation of [DevFSOperations] that speaks to the
+/// vm service.
+class ServiceProtocolDevFSOperations implements DevFSOperations {
+  ServiceProtocolDevFSOperations(this.vmService);
 
   final VMService vmService;
+
+  @override
+  Future<Uri> create(String fsName) async {
+    final Map<String, dynamic> response = await vmService.vm.createDevFS(fsName);
+    return Uri.parse(response['uri']);
+  }
+
+  @override
+  Future<dynamic> destroy(String fsName) async {
+    await vmService.vm.deleteDevFS(fsName);
+  }
+
+  @override
+  Future<dynamic> writeFile(String fsName, Uri deviceUri, DevFSContent content) async {
+    List<int> bytes;
+    try {
+      bytes = await content.contentsAsBytes();
+    } catch (e) {
+      return e;
+    }
+    final String fileContents = base64.encode(bytes);
+    try {
+      return await vmService.vm.invokeRpcRaw(
+        '_writeDevFSFile',
+        params: <String, dynamic>{
+          'fsName': fsName,
+          'uri': deviceUri.toString(),
+          'fileContents': fileContents,
+        },
+      );
+    } catch (error) {
+      printTrace('DevFS: Failed to write $deviceUri: $error');
+    }
+  }
+}
+
+class DevFSException implements Exception {
+  DevFSException(this.message, [this.error, this.stackTrace]);
+  final String message;
+  final dynamic error;
+  final StackTrace stackTrace;
+}
+
+class _DevFSHttpWriter {
+  _DevFSHttpWriter(this.fsName, VMService serviceProtocol)
+    : httpAddress = serviceProtocol.httpAddress;
+
   final String fsName;
   final Uri httpAddress;
-  final HttpClient _client = HttpClient();
 
   static const int kMaxInFlight = 6;
 
   int _inFlight = 0;
   Map<Uri, DevFSContent> _outstanding;
   Completer<void> _completer;
-
-  Future<Uri> create(String fsName) async {
-    final Map<String, dynamic> response = await vmService.vm.createDevFS(fsName);
-    return Uri.parse(response['uri']);
-  }
-
-  Future<void> destroy(String fsName) async {
-    await vmService.vm.deleteDevFS(fsName);
-  }
+  final HttpClient _client = HttpClient();
 
   Future<void> write(Map<Uri, DevFSContent> entries) async {
     _client.maxConnectionsPerHost = kMaxInFlight;
@@ -259,9 +301,9 @@
       final HttpClientRequest request = await _client.putUrl(httpAddress);
       request.headers.removeAll(HttpHeaders.acceptEncodingHeader);
       request.headers.add('dev_fs_name', fsName);
-      request.headers.add('dev_fs_uri_b64',
-          base64.encode(utf8.encode(deviceUri.toString())));
-      request.add(content.contentsAsCompressedBytes());
+      request.headers.add('dev_fs_uri_b64', base64.encode(utf8.encode('$deviceUri')));
+      final Stream<List<int>> contents = content.contentsAsCompressedStream();
+      await request.addStream(contents);
       final HttpClientResponse response = await request.close();
       await response.drain<void>();
     } catch (error, trace) {
@@ -275,14 +317,6 @@
   }
 }
 
-class DevFSException implements Exception {
-  DevFSException(this.message, [this.error, this.stackTrace]);
-
-  final String message;
-  final dynamic error;
-  final StackTrace stackTrace;
-}
-
 // Basic statistics for DevFS update operation.
 class UpdateFSReport {
   UpdateFSReport({
@@ -319,7 +353,8 @@
     this.fsName,
     this.rootDirectory, {
     String packagesFilePath,
-  }) : _operations = DevFSOperations(serviceProtocol, fsName),
+  }) : _operations = ServiceProtocolDevFSOperations(serviceProtocol),
+       _httpWriter = _DevFSHttpWriter(fsName, serviceProtocol),
        _packagesFilePath = packagesFilePath ?? fs.path.join(rootDirectory.path, kPackagesFileName);
 
   DevFS.operations(
@@ -327,9 +362,11 @@
     this.fsName,
     this.rootDirectory, {
     String packagesFilePath,
-  }) : _packagesFilePath = packagesFilePath ?? fs.path.join(rootDirectory.path, kPackagesFileName);
+  }) : _httpWriter = null,
+       _packagesFilePath = packagesFilePath ?? fs.path.join(rootDirectory.path, kPackagesFileName);
 
   final DevFSOperations _operations;
+  final _DevFSHttpWriter _httpWriter;
   final String fsName;
   final Directory rootDirectory;
   String _packagesFilePath;
@@ -452,7 +489,7 @@
     printTrace('Updating files');
     if (dirtyEntries.isNotEmpty) {
       try {
-        await _operations.write(dirtyEntries);
+        await _httpWriter.write(dirtyEntries);
       } on SocketException catch (socketException, stackTrace) {
         printTrace('DevFS sync failed. Lost connection to device: $socketException');
         throw DevFSException('Lost connection to device.', socketException, stackTrace);
diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_build.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_build.dart
index 3dac651..00d95fa 100644
--- a/packages/flutter_tools/lib/src/fuchsia/fuchsia_build.dart
+++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_build.dart
@@ -52,7 +52,7 @@
 
   final Map<String, DevFSContent> assetEntries =
       Map<String, DevFSContent>.from(assets.entries);
-  writeBundle(fs.directory(assetDir), assetEntries);
+  await writeBundle(fs.directory(assetDir), assetEntries);
 
   final String appName = fuchsiaProject.project.manifest.appName;
   final String outDir = getFuchsiaBuildDirectory();
diff --git a/packages/flutter_tools/lib/src/resident_web_runner.dart b/packages/flutter_tools/lib/src/resident_web_runner.dart
index aa1b61d..1652628 100644
--- a/packages/flutter_tools/lib/src/resident_web_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_web_runner.dart
@@ -120,7 +120,8 @@
     if (build != 0) {
       throwToolExit('Error: Failed to build asset bundle');
     }
-    writeBundle(fs.directory(getAssetBuildDirectory()), assetBundle.entries);
+    await writeBundle(
+        fs.directory(getAssetBuildDirectory()), assetBundle.entries);
 
     // Step 2: Start an HTTP server
     _server = WebAssetServer(flutterProject, target, ipv6);
diff --git a/packages/flutter_tools/lib/src/web/web_device.dart b/packages/flutter_tools/lib/src/web/web_device.dart
index 3288e41..d8ce9ce 100644
--- a/packages/flutter_tools/lib/src/web/web_device.dart
+++ b/packages/flutter_tools/lib/src/web/web_device.dart
@@ -119,7 +119,7 @@
     if (build != 0) {
       throwToolExit('Error: Failed to build asset bundle');
     }
-    writeBundle(fs.directory(getAssetBuildDirectory()), assetBundle.entries);
+    await writeBundle(fs.directory(getAssetBuildDirectory()), assetBundle.entries);
 
     _package = package;
     _server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
diff --git a/packages/flutter_tools/test/asset_bundle_package_fonts_test.dart b/packages/flutter_tools/test/asset_bundle_package_fonts_test.dart
index 0d2a3a2..648a092 100644
--- a/packages/flutter_tools/test/asset_bundle_package_fonts_test.dart
+++ b/packages/flutter_tools/test/asset_bundle_package_fonts_test.dart
@@ -74,7 +74,7 @@
         final String entryKey = 'packages/$packageName/$packageFont';
         expect(bundle.entries.containsKey(entryKey), true);
         expect(
-          utf8.decode(bundle.entries[entryKey].contentsAsBytes()),
+          utf8.decode(await bundle.entries[entryKey].contentsAsBytes()),
           packageFont,
         );
       }
@@ -82,14 +82,14 @@
       for (String localFont in localFonts) {
         expect(bundle.entries.containsKey(localFont), true);
         expect(
-          utf8.decode(bundle.entries[localFont].contentsAsBytes()),
+          utf8.decode(await bundle.entries[localFont].contentsAsBytes()),
           localFont,
         );
       }
     }
 
     expect(
-      json.decode(utf8.decode(bundle.entries['FontManifest.json'].contentsAsBytes())),
+      json.decode(utf8.decode(await bundle.entries['FontManifest.json'].contentsAsBytes())),
       json.decode(expectedAssetManifest),
     );
   }
diff --git a/packages/flutter_tools/test/asset_bundle_package_test.dart b/packages/flutter_tools/test/asset_bundle_package_test.dart
index 1e67c11..2ea7946 100644
--- a/packages/flutter_tools/test/asset_bundle_package_test.dart
+++ b/packages/flutter_tools/test/asset_bundle_package_test.dart
@@ -79,14 +79,14 @@
         final String entryKey = Uri.encodeFull('packages/$packageName/$asset');
         expect(bundle.entries.containsKey(entryKey), true, reason: 'Cannot find key on bundle: $entryKey');
         expect(
-          utf8.decode(bundle.entries[entryKey].contentsAsBytes()),
+          utf8.decode(await bundle.entries[entryKey].contentsAsBytes()),
           asset,
         );
       }
     }
 
     expect(
-      utf8.decode(bundle.entries['AssetManifest.json'].contentsAsBytes()),
+      utf8.decode(await bundle.entries['AssetManifest.json'].contentsAsBytes()),
       expectedAssetManifest,
     );
   }
@@ -126,11 +126,11 @@
       expect(bundle.entries.length, 3); // LICENSE, AssetManifest, FontManifest
       const String expectedAssetManifest = '{}';
       expect(
-        utf8.decode(bundle.entries['AssetManifest.json'].contentsAsBytes()),
+        utf8.decode(await bundle.entries['AssetManifest.json'].contentsAsBytes()),
         expectedAssetManifest,
       );
       expect(
-        utf8.decode(bundle.entries['FontManifest.json'].contentsAsBytes()),
+        utf8.decode(await bundle.entries['FontManifest.json'].contentsAsBytes()),
         '[]',
       );
     }, overrides: <Type, Generator>{
@@ -153,11 +153,11 @@
       expect(bundle.entries.length, 3); // LICENSE, AssetManifest, FontManifest
       const String expectedAssetManifest = '{}';
       expect(
-        utf8.decode(bundle.entries['AssetManifest.json'].contentsAsBytes()),
+        utf8.decode(await bundle.entries['AssetManifest.json'].contentsAsBytes()),
         expectedAssetManifest,
       );
       expect(
-        utf8.decode(bundle.entries['FontManifest.json'].contentsAsBytes()),
+        utf8.decode(await bundle.entries['FontManifest.json'].contentsAsBytes()),
         '[]',
       );
     }, overrides: <Type, Generator>{
diff --git a/packages/flutter_tools/test/asset_bundle_test.dart b/packages/flutter_tools/test/asset_bundle_test.dart
index 6bd084e..6d99cde 100644
--- a/packages/flutter_tools/test/asset_bundle_test.dart
+++ b/packages/flutter_tools/test/asset_bundle_test.dart
@@ -50,7 +50,7 @@
       expect(bundle.entries.length, 1);
       const String expectedAssetManifest = '{}';
       expect(
-        utf8.decode(bundle.entries['AssetManifest.json'].contentsAsBytes()),
+        utf8.decode(await bundle.entries['AssetManifest.json'].contentsAsBytes()),
         expectedAssetManifest,
       );
     }, overrides: <Type, Generator>{
diff --git a/packages/flutter_tools/test/asset_bundle_variant_test.dart b/packages/flutter_tools/test/asset_bundle_variant_test.dart
index 77bdbfd..9a7b9f5 100644
--- a/packages/flutter_tools/test/asset_bundle_variant_test.dart
+++ b/packages/flutter_tools/test/asset_bundle_variant_test.dart
@@ -76,7 +76,7 @@
       // The main asset file, /a/b/c/foo, and its variants exist.
       for (String asset in assets) {
         expect(bundle.entries.containsKey(asset), true);
-        expect(utf8.decode(bundle.entries[asset].contentsAsBytes()), asset);
+        expect(utf8.decode(await bundle.entries[asset].contentsAsBytes()), asset);
       }
 
       fs.file(fixPath('a/b/c/foo')).deleteSync();
@@ -88,7 +88,7 @@
       expect(bundle.entries.containsKey('a/b/c/foo'), false);
       for (String asset in assets.skip(1)) {
         expect(bundle.entries.containsKey(asset), true);
-        expect(utf8.decode(bundle.entries[asset].contentsAsBytes()), asset);
+        expect(utf8.decode(await bundle.entries[asset].contentsAsBytes()), asset);
       }
     }, overrides: <Type, Generator>{
       FileSystem: () => testFileSystem,
diff --git a/packages/flutter_tools/test/asset_test.dart b/packages/flutter_tools/test/asset_test.dart
index af961eb..bb8f259 100644
--- a/packages/flutter_tools/test/asset_test.dart
+++ b/packages/flutter_tools/test/asset_test.dart
@@ -7,7 +7,6 @@
 import 'package:flutter_tools/src/asset.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/cache.dart';
-import 'package:flutter_tools/src/convert.dart';
 
 import 'src/common.dart';
 import 'src/context.dart';
@@ -66,5 +65,5 @@
 }
 
 Future<String> getValueAsString(String key, AssetBundle asset) async {
-  return utf8.decode(asset.entries[key].contentsAsBytes());
+  return String.fromCharCodes(await asset.entries[key].contentsAsBytes());
 }
diff --git a/packages/flutter_tools/test/devfs_test.dart b/packages/flutter_tools/test/devfs_test.dart
index 1887e74..b8d93d4 100644
--- a/packages/flutter_tools/test/devfs_test.dart
+++ b/packages/flutter_tools/test/devfs_test.dart
@@ -4,17 +4,15 @@
 
 import 'dart:async';
 import 'dart:convert';
+import 'dart:io'; // ignore: dart_io_import
 
 import 'package:file/file.dart';
 import 'package:file/memory.dart';
-import 'package:flutter_tools/src/base/context.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/io.dart';
-import 'package:flutter_tools/src/base/platform.dart';
 import 'package:flutter_tools/src/devfs.dart';
 import 'package:flutter_tools/src/vmservice.dart';
 import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
-import 'package:mockito/mockito.dart';
 
 import 'src/common.dart';
 import 'src/context.dart';
@@ -22,39 +20,17 @@
 
 void main() {
   FileSystem fs;
-  MockPlatform mockPlatform;
+  String filePath;
+  Directory tempDir;
+  String basePath;
+  DevFS devFS;
 
-  setUp(() {
-    fs = MemoryFileSystem(style: FileSystemStyle.posix);
-    mockPlatform = MockPlatform();
-    when(mockPlatform.pathSeparator).thenReturn('/');
-    when(mockPlatform.isWindows).thenReturn(false);
+  setUpAll(() {
+    fs = MemoryFileSystem();
+    filePath = fs.path.join('lib', 'foo.txt');
   });
 
   group('DevFSContent', () {
-    test('copyToFile', () {
-      final String filePath = fs.path.join('lib', 'foo.txt');
-      final File file = fs.file(filePath)
-        ..createSync(recursive: true)
-        ..writeAsStringSync('hello, world');
-      final DevFSByteContent byteContent = DevFSByteContent(<int>[4, 5, 6]);
-      final DevFSStringContent stringContent = DevFSStringContent('some string');
-      final DevFSFileContent fileContent = DevFSFileContent(file);
-
-      final File byteDestination = fs.file('byte_dest');
-      final File stringDestination = fs.file('string_dest');
-      final File fileDestination = fs.file('file_dest');
-
-      byteContent.copyToFile(byteDestination);
-      expect(byteDestination.readAsBytesSync(), <int>[4, 5, 6]);
-
-      stringContent.copyToFile(stringDestination);
-      expect(stringDestination.readAsStringSync(), 'some string');
-
-      fileContent.copyToFile(fileDestination);
-      expect(fileDestination.readAsStringSync(), 'hello, world');
-    });
-
     test('bytes', () {
       final DevFSByteContent content = DevFSByteContent(<int>[4, 5, 6]);
       expect(content.bytes, orderedEquals(<int>[4, 5, 6]));
@@ -65,7 +41,6 @@
       expect(content.isModified, isTrue);
       expect(content.isModified, isFalse);
     });
-
     test('string', () {
       final DevFSStringContent content = DevFSStringContent('some string');
       expect(content.string, 'some string');
@@ -83,9 +58,7 @@
       expect(content.isModified, isTrue);
       expect(content.isModified, isFalse);
     });
-
     testUsingContext('file', () async {
-      final String filePath = fs.path.join('lib', 'foo.txt');
       final File file = fs.file(filePath);
       final DevFSFileContent content = DevFSFileContent(file);
       expect(content.isModified, isFalse);
@@ -100,9 +73,10 @@
       expect(content.isModifiedAfter(null), isTrue);
 
       file.writeAsBytesSync(<int>[2, 3, 4], flush: true);
+      expect(content.fileDependencies, <String>[filePath]);
       expect(content.isModified, isTrue);
       expect(content.isModified, isFalse);
-      expect(content.contentsAsBytes(), <int>[2, 3, 4]);
+      expect(await content.contentsAsBytes(), <int>[2, 3, 4]);
       updateFileModificationTime(file.path, fiveSecondsAgo, 0);
       expect(content.isModified, isFalse);
       expect(content.isModified, isFalse);
@@ -113,56 +87,36 @@
       expect(content.isModified, isFalse);
     }, overrides: <Type, Generator>{
       FileSystem: () => fs,
-    }, skip: platform.isWindows); // Still flaky, but only on CI :(
+    }, skip: Platform.isWindows); // TODO(jonahwilliams): fix or disable this functionality.
   });
 
   group('devfs remote', () {
-    DevFS devFS;
-    MockResidentCompiler residentCompiler;
-    MockDevFSOperations mockDevFSOperations;
-    int created;
-    int destroyed;
-    List<String> writtenFiles;
-    bool exists;
+    MockVMService vmService;
+    final MockResidentCompiler residentCompiler = MockResidentCompiler();
 
-    setUp(() async {
-      mockDevFSOperations = MockDevFSOperations();
-      devFS = DevFS.operations(mockDevFSOperations, 'test', fs.currentDirectory);
-      residentCompiler = MockResidentCompiler();
-      created = 0;
-      destroyed = 0;
-      exists = false;
-      writtenFiles = <String>[];
-      when(mockDevFSOperations.create('test')).thenAnswer((Invocation invocation) async {
-        if (exists) {
-          throw rpc.RpcException(1001, 'already exists');
-        }
-        exists = true;
-        created += 1;
-        return Uri.parse(InternetAddress.loopbackIPv4.toString());
-      });
-      when(mockDevFSOperations.destroy('test')).thenAnswer((Invocation invocation) async {
-        exists = false;
-        destroyed += 1;
-      });
-      when(mockDevFSOperations.write(any)).thenAnswer((Invocation invocation) async {
-        final Map<Uri, DevFSContent> entries = invocation.positionalArguments.first;
-        writtenFiles.addAll(entries.keys.map((Uri uri) => uri.toFilePath()));
-      });
+    setUpAll(() async {
+      tempDir = _newTempDir(fs);
+      basePath = tempDir.path;
+      vmService = MockVMService();
+      await vmService.setUp();
+    });
+    tearDownAll(() async {
+      await vmService.tearDown();
+      _cleanupTempDirs();
     });
 
     testUsingContext('create dev file system', () async {
       // simulate workspace
-      final String filePath = fs.path.join('lib', 'foo.txt');
-      final File file = fs.file(filePath);
+      final File file = fs.file(fs.path.join(basePath, filePath));
       await file.parent.create(recursive: true);
       file.writeAsBytesSync(<int>[1, 2, 3]);
 
       // simulate package
       await _createPackage(fs, 'somepkg', 'somefile.txt');
-      await devFS.create();
 
-      expect(created, 1);
+      devFS = DevFS(vmService, 'test', tempDir);
+      await devFS.create();
+      vmService.expectMessages(<String>['create test']);
       expect(devFS.assetPathsToEvict, isEmpty);
 
       final UpdateFSReport report = await devFS.update(
@@ -172,8 +126,9 @@
         trackWidgetCreation: false,
         invalidatedFiles: <Uri>[],
       );
-
-      expect(writtenFiles.single, contains('foo.txt.dill'));
+      vmService.expectMessages(<String>[
+        'writeFile test lib/foo.txt.dill',
+      ]);
       expect(devFS.assetPathsToEvict, isEmpty);
       expect(report.syncedBytes, 22);
       expect(report.success, true);
@@ -182,8 +137,9 @@
     });
 
     testUsingContext('delete dev file system', () async {
+      expect(vmService.messages, isEmpty, reason: 'prior test timeout');
       await devFS.destroy();
-      expect(destroyed, 1);
+      vmService.expectMessages(<String>['destroy test']);
       expect(devFS.assetPathsToEvict, isEmpty);
     }, overrides: <Type, Generator>{
       FileSystem: () => fs,
@@ -191,27 +147,26 @@
 
     testUsingContext('cleanup preexisting file system', () async {
       // simulate workspace
-      final String filePath = fs.path.join('lib', 'foo.txt');
-      final File file = fs.file(filePath);
+      final File file = fs.file(fs.path.join(basePath, filePath));
       await file.parent.create(recursive: true);
       file.writeAsBytesSync(<int>[1, 2, 3]);
 
       // simulate package
       await _createPackage(fs, 'somepkg', 'somefile.txt');
 
+      devFS = DevFS(vmService, 'test', tempDir);
       await devFS.create();
-      expect(created, 1);
+      vmService.expectMessages(<String>['create test']);
       expect(devFS.assetPathsToEvict, isEmpty);
 
       // Try to create again.
       await devFS.create();
-      expect(created, 2);
-      expect(destroyed, 1);
+      vmService.expectMessages(<String>['create test', 'destroy test', 'create test']);
       expect(devFS.assetPathsToEvict, isEmpty);
 
       // Really destroy.
       await devFS.destroy();
-      expect(destroyed, 2);
+      vmService.expectMessages(<String>['destroy test']);
       expect(devFS.assetPathsToEvict, isEmpty);
     }, overrides: <Type, Generator>{
       FileSystem: () => fs,
@@ -219,20 +174,113 @@
   });
 }
 
-class MockVMService extends Mock implements VMService {}
+class MockVMService extends BasicMock implements VMService {
+  MockVMService() {
+    _vm = MockVM(this);
+  }
 
-class MockDevFSOperations extends Mock implements DevFSOperations {}
+  Uri _httpAddress;
+  HttpServer _server;
+  MockVM _vm;
 
-class MockPlatform extends Mock implements Platform {}
+  @override
+  Uri get httpAddress => _httpAddress;
 
+  @override
+  VM get vm => _vm;
+
+  Future<void> setUp() async {
+    try {
+      _server = await HttpServer.bind(InternetAddress.loopbackIPv6, 0);
+      _httpAddress = Uri.parse('http://[::1]:${_server.port}');
+    } on SocketException {
+      // Fall back to IPv4 if the host doesn't support binding to IPv6 localhost
+      _server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
+      _httpAddress = Uri.parse('http://127.0.0.1:${_server.port}');
+    }
+    _server.listen((HttpRequest request) {
+      final String fsName = request.headers.value('dev_fs_name');
+      final String devicePath = utf8.decode(base64.decode(request.headers.value('dev_fs_uri_b64')));
+      messages.add('writeFile $fsName $devicePath');
+      request.drain<List<int>>().then<void>((List<int> value) {
+        request.response
+          ..write('Got it')
+          ..close();
+      });
+    });
+  }
+
+  Future<void> tearDown() async {
+    await _server?.close();
+  }
+
+  @override
+  dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
+}
+
+class MockVM implements VM {
+  MockVM(this._service);
+
+  final MockVMService _service;
+  final Uri _baseUri = Uri.parse('file:///tmp/devfs/test');
+  bool _devFSExists = false;
+
+  static const int kFileSystemAlreadyExists = 1001;
+
+  @override
+  Future<Map<String, dynamic>> createDevFS(String fsName) async {
+    _service.messages.add('create $fsName');
+    if (_devFSExists) {
+      throw rpc.RpcException(kFileSystemAlreadyExists, 'File system already exists');
+    }
+    _devFSExists = true;
+    return <String, dynamic>{'uri': '$_baseUri'};
+  }
+
+  @override
+  Future<Map<String, dynamic>> deleteDevFS(String fsName) async {
+    _service.messages.add('destroy $fsName');
+    _devFSExists = false;
+    return <String, dynamic>{'type': 'Success'};
+  }
+
+  @override
+  Future<Map<String, dynamic>> invokeRpcRaw(
+    String method, {
+    Map<String, dynamic> params = const <String, dynamic>{},
+    Duration timeout,
+    bool timeoutFatal = true,
+  }) async {
+    _service.messages.add('$method $params');
+    return <String, dynamic>{'success': true};
+  }
+
+  @override
+  dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
+}
+
+
+final List<Directory> _tempDirs = <Directory>[];
 final Map <String, Uri> _packages = <String, Uri>{};
 
+Directory _newTempDir(FileSystem fs) {
+  final Directory tempDir = fs.systemTempDirectory.createTempSync('flutter_devfs${_tempDirs.length}_test.');
+  _tempDirs.add(tempDir);
+  return tempDir;
+}
+
+void _cleanupTempDirs() {
+  while (_tempDirs.isNotEmpty)
+    tryToDelete(_tempDirs.removeLast());
+}
 
 Future<void> _createPackage(FileSystem fs, String pkgName, String pkgFileName, { bool doubleSlash = false }) async {
-  String pkgFilePath = fs.path.join(pkgName, 'lib', pkgFileName);
+  final Directory pkgTempDir = _newTempDir(fs);
+  String pkgFilePath = fs.path.join(pkgTempDir.path, pkgName, 'lib', pkgFileName);
   if (doubleSlash) {
     // Force two separators into the path.
-    pkgFilePath = fs.path.join(pkgName, 'lib', pkgFileName);
+    final String doubleSlash = fs.path.separator + fs.path.separator;
+    pkgFilePath = pkgTempDir.path + doubleSlash + fs.path.join(pkgName, 'lib', pkgFileName);
   }
   final File pkgFile = fs.file(pkgFilePath);
   await pkgFile.parent.create(recursive: true);
@@ -242,5 +290,6 @@
   _packages.forEach((String pkgName, Uri pkgUri) {
     sb.writeln('$pkgName:$pkgUri');
   });
-  fs.file('.packages').writeAsStringSync(sb.toString());
+  fs.file(fs.path.join(_tempDirs[0].path, '.packages')).writeAsStringSync(sb.toString());
 }
+
diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart
index 1d1da52..8c318bc 100644
--- a/packages/flutter_tools/test/src/mocks.dart
+++ b/packages/flutter_tools/test/src/mocks.dart
@@ -14,6 +14,7 @@
 import 'package:flutter_tools/src/base/platform.dart';
 import 'package:flutter_tools/src/build_info.dart';
 import 'package:flutter_tools/src/compile.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';
@@ -477,6 +478,30 @@
   }
 }
 
+class MockDevFSOperations extends BasicMock implements DevFSOperations {
+  Map<Uri, DevFSContent> devicePathToContent = <Uri, DevFSContent>{};
+
+  @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, Uri deviceUri, DevFSContent content) async {
+    String message = 'writeFile $fsName $deviceUri';
+    if (content is DevFSFileContent) {
+      message += ' ${content.file.path}';
+    }
+    messages.add(message);
+    devicePathToContent[deviceUri] = content;
+  }
+}
 
 class MockResidentCompiler extends BasicMock implements ResidentCompiler {
   @override