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(); -}