Commit 0295def2 authored by Dan Rubel's avatar Dan Rubel Committed by GitHub

Refactor DevFS for kernel code (#7529)

Refactor DevFS so that it's easier to add new types of content such as kernel code
* add tests for DevFS package scanning
* add tests for DevFS over VMService protocol
* which covers _DevFSHttpWriter and ServiceProtocolDevFSOperations
* replace AssetBundleEntry and DevFSEntry with DevFSContent
* refactor to cleanup common code and replace some fields with locals
* rework .package file generation refactor away DevFSOperations.writeSource
* only update .package file if it has changed
* only write/delete/evict assets that have been changed/removed
parent 9573bc14
...@@ -12,45 +12,20 @@ import 'package:yaml/yaml.dart'; ...@@ -12,45 +12,20 @@ import 'package:yaml/yaml.dart';
import 'base/file_system.dart'; import 'base/file_system.dart';
import 'build_info.dart'; import 'build_info.dart';
import 'cache.dart'; import 'cache.dart';
import 'devfs.dart';
import 'dart/package_map.dart'; import 'dart/package_map.dart';
import 'globals.dart'; import 'globals.dart';
/// An entry in an asset bundle.
class AssetBundleEntry {
/// An entry backed by a File.
AssetBundleEntry.fromFile(this.archivePath, this.file)
: _contents = null;
/// An entry backed by a String.
AssetBundleEntry.fromString(this.archivePath, this._contents)
: file = null;
/// The path within the bundle.
final String archivePath;
/// The payload.
List<int> contentsAsBytes() {
if (_contents != null) {
return UTF8.encode(_contents);
} else {
return file.readAsBytesSync();
}
}
bool get isStringEntry => _contents != null;
int get contentsLength => _contents.length;
final File file;
final String _contents;
}
/// A bundle of assets. /// A bundle of assets.
class AssetBundle { class AssetBundle {
final Set<AssetBundleEntry> entries = new Set<AssetBundleEntry>(); final Map<String, DevFSContent> entries = <String, DevFSContent>{};
static const String defaultManifestPath = 'flutter.yaml'; static const String defaultManifestPath = 'flutter.yaml';
static const String _kAssetManifestJson = 'AssetManifest.json';
static const String _kFontManifestJson = 'FontManifest.json';
static const String _kFontSetMaterial = 'material'; static const String _kFontSetMaterial = 'material';
static const String _kFontSetRoboto = 'roboto'; static const String _kFontSetRoboto = 'roboto';
static const String _kLICENSE = 'LICENSE';
bool _fixed = false; bool _fixed = false;
DateTime _lastBuildTimestamp; DateTime _lastBuildTimestamp;
...@@ -73,8 +48,7 @@ class AssetBundle { ...@@ -73,8 +48,7 @@ class AssetBundle {
continue; continue;
final String assetPath = path.join(projectRoot, asset); final String assetPath = path.join(projectRoot, asset);
final String archivePath = asset; final String archivePath = asset;
entries.add( entries[archivePath] = new DevFSFileContent(fs.file(assetPath));
new AssetBundleEntry.fromFile(archivePath, fs.file(assetPath)));
} }
} }
...@@ -111,7 +85,7 @@ class AssetBundle { ...@@ -111,7 +85,7 @@ class AssetBundle {
} }
if (manifest == null) { if (manifest == null) {
// No manifest file found for this application. // No manifest file found for this application.
entries.add(new AssetBundleEntry.fromString('AssetManifest.json', '{}')); entries[_kAssetManifestJson] = new DevFSStringContent('{}');
return 0; return 0;
} }
if (manifest != null) { if (manifest != null) {
...@@ -141,16 +115,11 @@ class AssetBundle { ...@@ -141,16 +115,11 @@ class AssetBundle {
manifestDescriptor['uses-material-design']; manifestDescriptor['uses-material-design'];
for (_Asset asset in assetVariants.keys) { for (_Asset asset in assetVariants.keys) {
AssetBundleEntry assetEntry = _createAssetEntry(asset); assert(asset.assetFileExists);
if (assetEntry == null) entries[asset.assetEntry] = new DevFSFileContent(asset.assetFile);
return 1;
entries.add(assetEntry);
for (_Asset variant in assetVariants[asset]) { for (_Asset variant in assetVariants[asset]) {
AssetBundleEntry variantEntry = _createAssetEntry(variant); assert(variant.assetFileExists);
if (variantEntry == null) entries[variant.assetEntry] = new DevFSFileContent(variant.assetFile);
return 1;
entries.add(variantEntry);
} }
} }
...@@ -161,29 +130,27 @@ class AssetBundle { ...@@ -161,29 +130,27 @@ class AssetBundle {
materialAssets.addAll(_getMaterialAssets(_kFontSetRoboto)); materialAssets.addAll(_getMaterialAssets(_kFontSetRoboto));
} }
for (_Asset asset in materialAssets) { for (_Asset asset in materialAssets) {
AssetBundleEntry assetEntry = _createAssetEntry(asset); assert(asset.assetFileExists);
if (assetEntry == null) entries[asset.assetEntry] = new DevFSFileContent(asset.assetFile);
return 1;
entries.add(assetEntry);
} }
entries.add(_createAssetManifest(assetVariants)); entries[_kAssetManifestJson] = _createAssetManifest(assetVariants);
AssetBundleEntry fontManifest = DevFSContent fontManifest =
_createFontManifest(manifestDescriptor, usesMaterialDesign, includeDefaultFonts, includeRobotoFonts); _createFontManifest(manifestDescriptor, usesMaterialDesign, includeDefaultFonts, includeRobotoFonts);
if (fontManifest != null) if (fontManifest != null)
entries.add(fontManifest); entries[_kFontManifestJson] = fontManifest;
// TODO(ianh): Only do the following line if we've changed packages or if our LICENSE file changed // TODO(ianh): Only do the following line if we've changed packages or if our LICENSE file changed
entries.add(await _obtainLicenses(packageMap, assetBasePath, reportPackages: reportLicensedPackages)); entries[_kLICENSE] = await _obtainLicenses(packageMap, assetBasePath, reportPackages: reportLicensedPackages);
return 0; return 0;
} }
void dump() { void dump() {
printTrace('Dumping AssetBundle:'); printTrace('Dumping AssetBundle:');
for (AssetBundleEntry entry in entries) { for (String archivePath in entries.keys.toList()..sort()) {
printTrace(entry.archivePath); printTrace(archivePath);
} }
} }
} }
...@@ -256,8 +223,8 @@ List<_Asset> _getMaterialAssets(String fontSet) { ...@@ -256,8 +223,8 @@ List<_Asset> _getMaterialAssets(String fontSet) {
final String _licenseSeparator = '\n' + ('-' * 80) + '\n'; final String _licenseSeparator = '\n' + ('-' * 80) + '\n';
/// Returns a AssetBundleEntry representing the license file. /// Returns a DevFSContent representing the license file.
Future<AssetBundleEntry> _obtainLicenses( Future<DevFSContent> _obtainLicenses(
PackageMap packageMap, PackageMap packageMap,
String assetBase, String assetBase,
{ bool reportPackages } { bool reportPackages }
...@@ -323,17 +290,10 @@ Future<AssetBundleEntry> _obtainLicenses( ...@@ -323,17 +290,10 @@ Future<AssetBundleEntry> _obtainLicenses(
final String combinedLicenses = combinedLicensesList.join(_licenseSeparator); final String combinedLicenses = combinedLicensesList.join(_licenseSeparator);
return new AssetBundleEntry.fromString('LICENSE', combinedLicenses); return new DevFSStringContent(combinedLicenses);
}
/// Create a [AssetBundleEntry] from the given [_Asset]; the asset must exist.
AssetBundleEntry _createAssetEntry(_Asset asset) {
assert(asset.assetFileExists);
return new AssetBundleEntry.fromFile(asset.assetEntry, asset.assetFile);
} }
AssetBundleEntry _createAssetManifest(Map<_Asset, List<_Asset>> assetVariants) { DevFSContent _createAssetManifest(Map<_Asset, List<_Asset>> assetVariants) {
Map<String, List<String>> json = <String, List<String>>{}; Map<String, List<String>> json = <String, List<String>>{};
for (_Asset main in assetVariants.keys) { for (_Asset main in assetVariants.keys) {
List<String> variants = <String>[]; List<String> variants = <String>[];
...@@ -341,10 +301,10 @@ AssetBundleEntry _createAssetManifest(Map<_Asset, List<_Asset>> assetVariants) { ...@@ -341,10 +301,10 @@ AssetBundleEntry _createAssetManifest(Map<_Asset, List<_Asset>> assetVariants) {
variants.add(variant.relativePath); variants.add(variant.relativePath);
json[main.relativePath] = variants; json[main.relativePath] = variants;
} }
return new AssetBundleEntry.fromString('AssetManifest.json', JSON.encode(json)); return new DevFSStringContent(JSON.encode(json));
} }
AssetBundleEntry _createFontManifest(Map<String, dynamic> manifestDescriptor, DevFSContent _createFontManifest(Map<String, dynamic> manifestDescriptor,
bool usesMaterialDesign, bool usesMaterialDesign,
bool includeDefaultFonts, bool includeDefaultFonts,
bool includeRobotoFonts) { bool includeRobotoFonts) {
...@@ -358,7 +318,7 @@ AssetBundleEntry _createFontManifest(Map<String, dynamic> manifestDescriptor, ...@@ -358,7 +318,7 @@ AssetBundleEntry _createFontManifest(Map<String, dynamic> manifestDescriptor,
fonts.addAll(manifestDescriptor['fonts']); fonts.addAll(manifestDescriptor['fonts']);
if (fonts.isEmpty) if (fonts.isEmpty)
return null; return null;
return new AssetBundleEntry.fromString('FontManifest.json', JSON.encode(fonts)); return new DevFSStringContent(JSON.encode(fonts));
} }
/// Given an assetBase location and a flutter.yaml manifest, return a map of /// Given an assetBase location and a flutter.yaml manifest, return a map of
......
This diff is collapsed.
...@@ -11,6 +11,7 @@ import 'base/common.dart'; ...@@ -11,6 +11,7 @@ import 'base/common.dart';
import 'base/file_system.dart'; import 'base/file_system.dart';
import 'base/process.dart'; import 'base/process.dart';
import 'dart/package_map.dart'; import 'dart/package_map.dart';
import 'devfs.dart';
import 'build_info.dart'; import 'build_info.dart';
import 'globals.dart'; import 'globals.dart';
import 'toolchain.dart'; import 'toolchain.dart';
...@@ -156,12 +157,12 @@ Future<Null> assemble({ ...@@ -156,12 +157,12 @@ Future<Null> assemble({
zipBuilder.entries.addAll(assetBundle.entries); zipBuilder.entries.addAll(assetBundle.entries);
if (snapshotFile != null) if (snapshotFile != null)
zipBuilder.addEntry(new AssetBundleEntry.fromFile(_kSnapshotKey, snapshotFile)); zipBuilder.entries[_kSnapshotKey] = new DevFSFileContent(snapshotFile);
ensureDirectoryExists(outputPath); ensureDirectoryExists(outputPath);
printTrace('Encoding zip file to $outputPath'); printTrace('Encoding zip file to $outputPath');
zipBuilder.createZip(fs.file(outputPath), fs.directory(workingDirPath)); await zipBuilder.createZip(fs.file(outputPath), fs.directory(workingDirPath));
printTrace('Built $outputPath.'); printTrace('Built $outputPath.');
} }
...@@ -273,7 +273,7 @@ class HotRunner extends ResidentRunner { ...@@ -273,7 +273,7 @@ class HotRunner extends ResidentRunner {
return false; return false;
} }
Status devFSStatus = logger.startProgress('Syncing files to device...'); Status devFSStatus = logger.startProgress('Syncing files to device...');
await _devFS.update(progressReporter: progressReporter, int bytes = await _devFS.update(progressReporter: progressReporter,
bundle: assetBundle, bundle: assetBundle,
bundleDirty: rebuildBundle, bundleDirty: rebuildBundle,
fileFilter: _dartDependencies); fileFilter: _dartDependencies);
...@@ -282,18 +282,19 @@ class HotRunner extends ResidentRunner { ...@@ -282,18 +282,19 @@ class HotRunner extends ResidentRunner {
// Clear the set after the sync so they are recomputed next time. // Clear the set after the sync so they are recomputed next time.
_dartDependencies = null; _dartDependencies = null;
} }
printTrace('Synced ${getSizeAsMB(_devFS.bytes)}.'); printTrace('Synced ${getSizeAsMB(bytes)}.');
return true; return true;
} }
Future<Null> _evictDirtyAssets() async { Future<Null> _evictDirtyAssets() async {
if (_devFS.dirtyAssetEntries.length == 0) if (_devFS.assetPathsToEvict.isEmpty)
return; return;
if (currentView.uiIsolate == null) if (currentView.uiIsolate == null)
throw 'Application isolate not found'; throw 'Application isolate not found';
for (DevFSEntry entry in _devFS.dirtyAssetEntries) { for (String assetPath in _devFS.assetPathsToEvict) {
await currentView.uiIsolate.flutterEvictAsset(entry.assetPath); await currentView.uiIsolate.flutterEvictAsset(assetPath);
} }
_devFS.assetPathsToEvict.clear();
} }
Future<Null> _cleanupDevFS() async { Future<Null> _cleanupDevFS() async {
......
...@@ -2,10 +2,12 @@ ...@@ -2,10 +2,12 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'package:archive/archive.dart'; import 'package:archive/archive.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'asset.dart'; import 'devfs.dart';
import 'base/file_system.dart'; import 'base/file_system.dart';
import 'base/process.dart'; import 'base/process.dart';
...@@ -20,27 +22,32 @@ abstract class ZipBuilder { ...@@ -20,27 +22,32 @@ abstract class ZipBuilder {
ZipBuilder._(); ZipBuilder._();
List<AssetBundleEntry> entries = <AssetBundleEntry>[]; Map<String, DevFSContent> entries = <String, DevFSContent>{};
void addEntry(AssetBundleEntry entry) => entries.add(entry);
void createZip(File outFile, Directory zipBuildDir); Future<Null> createZip(File outFile, Directory zipBuildDir);
} }
class _ArchiveZipBuilder extends ZipBuilder { class _ArchiveZipBuilder extends ZipBuilder {
_ArchiveZipBuilder() : super._(); _ArchiveZipBuilder() : super._();
@override @override
void createZip(File outFile, Directory zipBuildDir) { Future<Null> createZip(File outFile, Directory zipBuildDir) async {
Archive archive = new Archive(); Archive archive = new Archive();
for (AssetBundleEntry entry in entries) { final Completer<Null> finished = new Completer<Null>();
List<int> data = entry.contentsAsBytes(); int count = entries.length;
archive.addFile(new ArchiveFile.noCompress(entry.archivePath, data.length, data)); entries.forEach((String archivePath, DevFSContent content) {
} content.contentsAsBytes().then((List<int> data) {
archive.addFile(new ArchiveFile.noCompress(archivePath, data.length, data));
--count;
if (count == 0)
finished.complete();
});
});
await finished.future;
List<int> zipData = new ZipEncoder().encode(archive); List<int> zipData = new ZipEncoder().encode(archive);
outFile.writeAsBytesSync(zipData); await outFile.writeAsBytes(zipData);
} }
} }
...@@ -48,11 +55,11 @@ class _ZipToolBuilder extends ZipBuilder { ...@@ -48,11 +55,11 @@ class _ZipToolBuilder extends ZipBuilder {
_ZipToolBuilder() : super._(); _ZipToolBuilder() : super._();
@override @override
void createZip(File outFile, Directory zipBuildDir) { Future<Null> createZip(File outFile, Directory zipBuildDir) async {
// If there are no assets, then create an empty zip file. // If there are no assets, then create an empty zip file.
if (entries.isEmpty) { if (entries.isEmpty) {
List<int> zipData = new ZipEncoder().encode(new Archive()); List<int> zipData = new ZipEncoder().encode(new Archive());
outFile.writeAsBytesSync(zipData); await outFile.writeAsBytes(zipData);
return; return;
} }
...@@ -63,23 +70,33 @@ class _ZipToolBuilder extends ZipBuilder { ...@@ -63,23 +70,33 @@ class _ZipToolBuilder extends ZipBuilder {
zipBuildDir.deleteSync(recursive: true); zipBuildDir.deleteSync(recursive: true);
zipBuildDir.createSync(recursive: true); zipBuildDir.createSync(recursive: true);
for (AssetBundleEntry entry in entries) { final Completer<Null> finished = new Completer<Null>();
List<int> data = entry.contentsAsBytes(); int count = entries.length;
File file = fs.file(path.join(zipBuildDir.path, entry.archivePath)); entries.forEach((String archivePath, DevFSContent content) {
content.contentsAsBytes().then((List<int> data) {
File file = fs.file(path.join(zipBuildDir.path, archivePath));
file.parent.createSync(recursive: true); file.parent.createSync(recursive: true);
file.writeAsBytesSync(data); file.writeAsBytes(data).then((_) {
} --count;
if (count == 0)
if (_getCompressedNames().isNotEmpty) { finished.complete();
});
});
});
await finished.future;
final Iterable<String> compressedNames = _getCompressedNames();
if (compressedNames.isNotEmpty) {
runCheckedSync( runCheckedSync(
<String>['zip', '-q', outFile.absolute.path]..addAll(_getCompressedNames()), <String>['zip', '-q', outFile.absolute.path]..addAll(compressedNames),
workingDirectory: zipBuildDir.path workingDirectory: zipBuildDir.path
); );
} }
if (_getStoredNames().isNotEmpty) { final Iterable<String> storedNames = _getStoredNames();
if (storedNames.isNotEmpty) {
runCheckedSync( runCheckedSync(
<String>['zip', '-q', '-0', outFile.absolute.path]..addAll(_getStoredNames()), <String>['zip', '-q', '-0', outFile.absolute.path]..addAll(storedNames),
workingDirectory: zipBuildDir.path workingDirectory: zipBuildDir.path
); );
} }
...@@ -87,21 +104,14 @@ class _ZipToolBuilder extends ZipBuilder { ...@@ -87,21 +104,14 @@ class _ZipToolBuilder extends ZipBuilder {
static const List<String> _kNoCompressFileExtensions = const <String>['.png', '.jpg']; static const List<String> _kNoCompressFileExtensions = const <String>['.png', '.jpg'];
bool isAssetCompressed(AssetBundleEntry entry) { bool isAssetCompressed(String archivePath) {
return !_kNoCompressFileExtensions.any( return !_kNoCompressFileExtensions.any(
(String extension) => entry.archivePath.endsWith(extension) (String extension) => archivePath.endsWith(extension)
); );
} }
Iterable<String> _getCompressedNames() { Iterable<String> _getCompressedNames() => entries.keys.where(isAssetCompressed);
return entries
.where(isAssetCompressed)
.map((AssetBundleEntry entry) => entry.archivePath);
}
Iterable<String> _getStoredNames() { Iterable<String> _getStoredNames() => entries.keys
return entries .where((String archivePath) => !isAssetCompressed(archivePath));
.where((AssetBundleEntry entry) => !isAssetCompressed(entry))
.map((AssetBundleEntry entry) => entry.archivePath);
}
} }
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter_tools/src/asset.dart'; import 'package:flutter_tools/src/asset.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/devfs.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:test/test.dart'; import 'package:test/test.dart';
...@@ -37,29 +38,26 @@ void main() { ...@@ -37,29 +38,26 @@ void main() {
AssetBundle ab = new AssetBundle.fixed('', 'apple.txt'); AssetBundle ab = new AssetBundle.fixed('', 'apple.txt');
expect(ab.entries, isNotEmpty); expect(ab.entries, isNotEmpty);
expect(ab.entries.length, 1); expect(ab.entries.length, 1);
AssetBundleEntry entry = ab.entries.first; String archivePath = ab.entries.keys.first;
expect(entry, isNotNull); expect(archivePath, isNotNull);
expect(entry.archivePath, 'apple.txt'); expect(archivePath, 'apple.txt');
}); });
test('two entries', () async { test('two entries', () async {
AssetBundle ab = new AssetBundle.fixed('', 'apple.txt,packages/flutter_gallery_assets/shrine/products/heels.png'); AssetBundle ab = new AssetBundle.fixed('', 'apple.txt,packages/flutter_gallery_assets/shrine/products/heels.png');
expect(ab.entries, isNotEmpty); expect(ab.entries, isNotEmpty);
expect(ab.entries.length, 2); expect(ab.entries.length, 2);
AssetBundleEntry firstEntry = ab.entries.first; List<String> archivePaths = ab.entries.keys.toList()..sort();
expect(firstEntry, isNotNull); expect(archivePaths[0], 'apple.txt');
expect(firstEntry.archivePath, 'apple.txt'); expect(archivePaths[1], 'packages/flutter_gallery_assets/shrine/products/heels.png');
AssetBundleEntry lastEntry = ab.entries.last;
expect(lastEntry, isNotNull);
expect(lastEntry.archivePath, 'packages/flutter_gallery_assets/shrine/products/heels.png');
}); });
test('file contents', () async { test('file contents', () async {
AssetBundle ab = new AssetBundle.fixed(projectRoot, assetPath); AssetBundle ab = new AssetBundle.fixed(projectRoot, assetPath);
expect(ab.entries, isNotEmpty); expect(ab.entries, isNotEmpty);
expect(ab.entries.length, 1); expect(ab.entries.length, 1);
AssetBundleEntry entry = ab.entries.first; String archivePath = ab.entries.keys.first;
expect(entry, isNotNull); DevFSContent content = ab.entries[archivePath];
expect(entry.archivePath, assetPath); expect(archivePath, assetPath);
expect(assetContents, UTF8.decode(entry.contentsAsBytes())); expect(assetContents, UTF8.decode(await content.contentsAsBytes()));
}); });
}); });
......
This diff is collapsed.
...@@ -13,6 +13,7 @@ import 'package:flutter_tools/src/ios/devices.dart'; ...@@ -13,6 +13,7 @@ import 'package:flutter_tools/src/ios/devices.dart';
import 'package:flutter_tools/src/ios/simulators.dart'; import 'package:flutter_tools/src/ios/simulators.dart';
import 'package:flutter_tools/src/runner/flutter_command.dart'; import 'package:flutter_tools/src/runner/flutter_command.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
class MockApplicationPackageStore extends ApplicationPackageStore { class MockApplicationPackageStore extends ApplicationPackageStore {
MockApplicationPackageStore() : super( MockApplicationPackageStore() : super(
...@@ -74,9 +75,16 @@ void applyMocksToCommand(FlutterCommand command) { ...@@ -74,9 +75,16 @@ void applyMocksToCommand(FlutterCommand command) {
..commandValidator = () => true; ..commandValidator = () => true;
} }
class MockDevFSOperations implements DevFSOperations { /// Common functionality for tracking mock interaction
class BasicMock {
final List<String> messages = new List<String>(); final List<String> messages = new List<String>();
void expectMessages(List<String> expectedMessages) {
List<String> actualMessages = new List<String>.from(messages);
messages.clear();
expect(actualMessages, unorderedEquals(expectedMessages));
}
bool contains(String match) { bool contains(String match) {
print('Checking for `$match` in:'); print('Checking for `$match` in:');
print(messages); print(messages);
...@@ -84,7 +92,9 @@ class MockDevFSOperations implements DevFSOperations { ...@@ -84,7 +92,9 @@ class MockDevFSOperations implements DevFSOperations {
messages.clear(); messages.clear();
return result; return result;
} }
}
class MockDevFSOperations extends BasicMock implements DevFSOperations {
@override @override
Future<Uri> create(String fsName) async { Future<Uri> create(String fsName) async {
messages.add('create $fsName'); messages.add('create $fsName');
...@@ -97,19 +107,12 @@ class MockDevFSOperations implements DevFSOperations { ...@@ -97,19 +107,12 @@ class MockDevFSOperations implements DevFSOperations {
} }
@override @override
Future<dynamic> writeFile(String fsName, DevFSEntry entry) async { Future<dynamic> writeFile(String fsName, String devicePath, DevFSContent content) async {
messages.add('writeFile $fsName ${entry.devicePath}'); messages.add('writeFile $fsName $devicePath');
}
@override
Future<dynamic> deleteFile(String fsName, DevFSEntry entry) async {
messages.add('deleteFile $fsName ${entry.devicePath}');
} }
@override @override
Future<dynamic> writeSource(String fsName, Future<dynamic> deleteFile(String fsName, String devicePath) async {
String devicePath, messages.add('deleteFile $fsName $devicePath');
String contents) async {
messages.add('writeSource $fsName $devicePath');
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment