| // Copyright 2014 The Flutter 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 'package:package_config/package_config.dart'; | |
| import 'package:process/process.dart'; | |
| import 'package:vm_service/vm_service.dart' as vm_service; | |
| import 'artifacts.dart'; | |
| import 'asset.dart'; | |
| import 'base/config.dart'; | |
| import 'base/context.dart'; | |
| import 'base/file_system.dart'; | |
| import 'base/io.dart'; | |
| import 'base/logger.dart'; | |
| import 'base/net.dart'; | |
| import 'base/os.dart'; | |
| import 'build_info.dart'; | |
| import 'build_system/tools/asset_transformer.dart'; | |
| import 'build_system/tools/shader_compiler.dart'; | |
| import 'compile.dart'; | |
| import 'convert.dart' show base64, utf8; | |
| import 'vmservice.dart'; | |
| class DevFSConfig { | |
| /// Should DevFS assume that symlink targets are stable? | |
| bool cacheSymlinks = false; | |
| /// Should DevFS assume that there are no symlinks to directories? | |
| bool noDirectorySymlinks = false; | |
| } | |
| DevFSConfig? get devFSConfig => context.get<DevFSConfig>(); | |
| /// Common superclass for content copied to the device. | |
| abstract class DevFSContent { | |
| /// Return true if the entry has been modified since it was last marked clean. | |
| bool get isModified; | |
| /// Mark the content as clean, resetting the [isModified] state. | |
| void markClean(); | |
| /// Return true if the entry has been modified after the given time. | |
| bool isModifiedAfter(DateTime time); | |
| int get size; | |
| Future<List<int>> contentsAsBytes(); | |
| Stream<List<int>> contentsAsStream(); | |
| Stream<List<int>> contentsAsCompressedStream(OperatingSystemUtils osUtils) { | |
| return osUtils.gzipLevel1Stream(contentsAsStream()); | |
| } | |
| } | |
| // File content to be copied to the device. | |
| class DevFSFileContent extends DevFSContent { | |
| DevFSFileContent(this.file); | |
| final FileSystemEntity file; | |
| File? _linkTarget; | |
| FileStat? _fileStat; | |
| File _getFile() { | |
| final File? linkTarget = _linkTarget; | |
| if (linkTarget != null) { | |
| return linkTarget; | |
| } | |
| if (file is Link) { | |
| // The link target. | |
| return file.fileSystem.file(file.resolveSymbolicLinksSync()); | |
| } | |
| return file as File; | |
| } | |
| (FileStat?, File?) _statFile() { | |
| File? linkTarget = _linkTarget; | |
| if (linkTarget != null) { | |
| // Stat the cached symlink target. | |
| final FileStat fileStat = linkTarget.statSync(); | |
| if (fileStat.type != FileSystemEntityType.notFound) { | |
| return (fileStat, linkTarget); | |
| } | |
| linkTarget = null; | |
| } | |
| final FileStat fileStat = file.statSync(); | |
| if (fileStat.type == FileSystemEntityType.notFound) { | |
| return (null, null); | |
| } | |
| if (fileStat.type == FileSystemEntityType.link) { | |
| try { | |
| final String resolved = file.resolveSymbolicLinksSync(); | |
| final File resolvedFile = file.fileSystem.file(resolved); | |
| // Stat the link target. | |
| final FileStat fileStat = resolvedFile.statSync(); | |
| if (fileStat.type == FileSystemEntityType.notFound) { | |
| return (null, null); | |
| } | |
| return (fileStat, resolvedFile); | |
| } on FileSystemException { | |
| return (null, null); | |
| } | |
| } | |
| return (fileStat, null); | |
| } | |
| @override | |
| bool get isModified { | |
| final (FileStat? currentStat, _) = _statFile(); | |
| if (_fileStat == null && currentStat == null) { | |
| return false; | |
| } | |
| return _fileStat == null || | |
| currentStat == null || | |
| currentStat.modified.isAfter(_fileStat!.modified); | |
| } | |
| @override | |
| void markClean() { | |
| final (FileStat? fileStat, File? linkTarget) = _statFile(); | |
| _fileStat = fileStat; | |
| if (linkTarget != null && (devFSConfig?.cacheSymlinks ?? false)) { | |
| _linkTarget = linkTarget; | |
| } else { | |
| _linkTarget = null; | |
| } | |
| } | |
| @override | |
| bool isModifiedAfter(DateTime time) { | |
| final (FileStat? currentStat, _) = _statFile(); | |
| if (_fileStat == null && currentStat == null) { | |
| return false; | |
| } | |
| return _fileStat == null || currentStat == null || currentStat.modified.isAfter(time); | |
| } | |
| @override | |
| int get size { | |
| final FileStat? stat = _fileStat ?? _statFile().$1; | |
| return stat?.size ?? 0; | |
| } | |
| @override | |
| Future<List<int>> contentsAsBytes() async => _getFile().readAsBytes(); | |
| @override | |
| Stream<List<int>> contentsAsStream() => _getFile().openRead(); | |
| } | |
| /// Byte content to be copied to the device. | |
| class DevFSByteContent extends DevFSContent { | |
| DevFSByteContent(this._bytes); | |
| final List<int> _bytes; | |
| final _creationTime = DateTime.now(); | |
| var _isModified = true; | |
| List<int> get bytes => _bytes; | |
| @override | |
| bool get isModified => _isModified; | |
| @override | |
| void markClean() { | |
| _isModified = false; | |
| } | |
| @override | |
| bool isModifiedAfter(DateTime time) { | |
| return _creationTime.isAfter(time); | |
| } | |
| @override | |
| int get size => _bytes.length; | |
| @override | |
| Future<List<int>> contentsAsBytes() async => _bytes; | |
| @override | |
| Stream<List<int>> contentsAsStream() => Stream<List<int>>.fromIterable(<List<int>>[_bytes]); | |
| } | |
| /// String content to be copied to the device. | |
| class DevFSStringContent extends DevFSByteContent { | |
| DevFSStringContent(String string) : _string = string, super(utf8.encode(string)); | |
| final String _string; | |
| String get string => _string; | |
| } | |
| /// A string compressing DevFSContent. | |
| /// | |
| /// A specialized DevFSContent similar to DevFSByteContent where the contents | |
| /// are the compressed bytes of a string. Its difference is that the original | |
| /// uncompressed string can be compared with directly without the indirection | |
| /// of a compute-expensive uncompress/decode and compress/encode to compare | |
| /// the strings. | |
| /// | |
| /// The `hintString` parameter is a zlib dictionary hinting mechanism to suggest | |
| /// the most common string occurrences to potentially assist with compression. | |
| class DevFSStringCompressingBytesContent extends DevFSContent { | |
| DevFSStringCompressingBytesContent(this._string, {String? hintString}) | |
| : _compressor = ZLibEncoder( | |
| dictionary: hintString == null ? null : utf8.encode(hintString), | |
| gzip: true, | |
| level: 9, | |
| ); | |
| final String _string; | |
| final ZLibEncoder _compressor; | |
| final _creationTime = DateTime.now(); | |
| var _isModified = true; | |
| late final List<int> bytes = _compressor.convert(utf8.encode(_string)); | |
| @override | |
| bool get isModified => _isModified; | |
| @override | |
| void markClean() { | |
| _isModified = false; | |
| } | |
| @override | |
| bool isModifiedAfter(DateTime time) { | |
| return _creationTime.isAfter(time); | |
| } | |
| @override | |
| int get size => bytes.length; | |
| @override | |
| Future<List<int>> contentsAsBytes() async => bytes; | |
| @override | |
| Stream<List<int>> contentsAsStream() => Stream<List<int>>.value(bytes); | |
| /// This checks the source string with another string. | |
| bool equals(String string) => _string == string; | |
| } | |
| class DevFSException implements Exception { | |
| DevFSException(this.message, [this.error, this.stackTrace]); | |
| final String message; | |
| final dynamic error; | |
| final StackTrace? stackTrace; | |
| @override | |
| String toString() => 'DevFSException($message, $error, $stackTrace)'; | |
| } | |
| /// Interface responsible for syncing asset files to a development device. | |
| abstract class DevFSWriter { | |
| /// Write the assets in [entries] to the target device. | |
| /// | |
| /// The keys of the map are relative from the [baseUri]. | |
| /// | |
| /// Throws a [DevFSException] if the process fails to complete. | |
| Future<void> write(Map<Uri, DevFSContent> entries, Uri baseUri, DevFSWriter parent); | |
| } | |
| class _DevFSHttpWriter implements DevFSWriter { | |
| _DevFSHttpWriter( | |
| this.fsName, | |
| FlutterVmService serviceProtocol, { | |
| required OperatingSystemUtils osUtils, | |
| required HttpClient httpClient, | |
| required Logger logger, | |
| Duration? uploadRetryThrottle, | |
| }) : httpAddress = serviceProtocol.httpAddress, | |
| _client = httpClient, | |
| _osUtils = osUtils, | |
| _uploadRetryThrottle = uploadRetryThrottle, | |
| _logger = logger; | |
| final HttpClient _client; | |
| final OperatingSystemUtils _osUtils; | |
| final Logger _logger; | |
| final Duration? _uploadRetryThrottle; | |
| final String fsName; | |
| final Uri? httpAddress; | |
| // 3 was chosen to try to limit the variance in the time it takes to execute | |
| // `await request.close()` since there is a known bug in Dart where it doesn't | |
| // always return a status code in response to a PUT request: | |
| // https://github.com/dart-lang/sdk/issues/43525. | |
| static const kMaxInFlight = 3; | |
| var _inFlight = 0; | |
| late Map<Uri, DevFSContent> _outstanding; | |
| late Completer<void> _completer; | |
| @override | |
| Future<void> write(Map<Uri, DevFSContent> entries, Uri devFSBase, [DevFSWriter? parent]) async { | |
| try { | |
| _client.maxConnectionsPerHost = kMaxInFlight; | |
| _completer = Completer<void>(); | |
| _outstanding = Map<Uri, DevFSContent>.of(entries); | |
| _scheduleWrites(); | |
| await _completer.future; | |
| } on SocketException catch (socketException, stackTrace) { | |
| _logger.printTrace('DevFS sync failed. Lost connection to device: $socketException'); | |
| throw DevFSException('Lost connection to device.', socketException, stackTrace); | |
| } on Exception catch (exception, stackTrace) { | |
| _logger.printError('Could not update files on device: $exception'); | |
| throw DevFSException('Sync failed', exception, stackTrace); | |
| } | |
| } | |
| void _scheduleWrites() { | |
| while ((_inFlight < kMaxInFlight) && (!_completer.isCompleted) && _outstanding.isNotEmpty) { | |
| final Uri deviceUri = _outstanding.keys.first; | |
| final DevFSContent content = _outstanding.remove(deviceUri)!; | |
| _startWrite(deviceUri, content, retry: 10); | |
| _inFlight += 1; | |
| } | |
| if ((_inFlight == 0) && (!_completer.isCompleted) && _outstanding.isEmpty) { | |
| _completer.complete(); | |
| } | |
| } | |
| Future<void> _startWrite(Uri deviceUri, DevFSContent content, {int retry = 0}) async { | |
| while (true) { | |
| try { | |
| 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'))); | |
| final Stream<List<int>> contents = content.contentsAsCompressedStream(_osUtils); | |
| await request.addStream(contents); | |
| // Once the bug in Dart is solved we can remove the timeout | |
| // (https://github.com/dart-lang/sdk/issues/43525). | |
| try { | |
| final HttpClientResponse response = await request.close().timeout( | |
| const Duration(seconds: 60), | |
| ); | |
| response.listen( | |
| (_) {}, | |
| onError: (dynamic error) { | |
| _logger.printTrace('error: $error'); | |
| }, | |
| cancelOnError: true, | |
| ); | |
| } on TimeoutException { | |
| request.abort(); | |
| // This should throw "HttpException: Request has been aborted". | |
| await request.done; | |
| // Just to be safe we rethrow the TimeoutException. | |
| rethrow; | |
| } | |
| break; | |
| } on Exception catch (error, trace) { | |
| if (!_completer.isCompleted) { | |
| _logger.printTrace('Error writing "$deviceUri" to DevFS: $error'); | |
| if (retry > 0) { | |
| retry--; | |
| _logger.printTrace('trying again in a few - $retry more attempts left'); | |
| await Future<void>.delayed(_uploadRetryThrottle ?? const Duration(milliseconds: 500)); | |
| continue; | |
| } | |
| _completer.completeError(error, trace); | |
| } | |
| } | |
| } | |
| _inFlight -= 1; | |
| _scheduleWrites(); | |
| } | |
| } | |
| // Basic statistics for DevFS update operation. | |
| class UpdateFSReport { | |
| UpdateFSReport({ | |
| bool success = false, | |
| int invalidatedSourcesCount = 0, | |
| int syncedBytes = 0, | |
| int scannedSourcesCount = 0, | |
| Duration compileDuration = Duration.zero, | |
| Duration transferDuration = Duration.zero, | |
| Duration findInvalidatedDuration = Duration.zero, | |
| bool hotReloadRejected = false, | |
| }) : _success = success, | |
| _invalidatedSourcesCount = invalidatedSourcesCount, | |
| _syncedBytes = syncedBytes, | |
| _scannedSourcesCount = scannedSourcesCount, | |
| _compileDuration = compileDuration, | |
| _transferDuration = transferDuration, | |
| _findInvalidatedDuration = findInvalidatedDuration, | |
| _hotReloadRejected = hotReloadRejected; | |
| bool get success => _success; | |
| int get invalidatedSourcesCount => _invalidatedSourcesCount; | |
| int get syncedBytes => _syncedBytes; | |
| int get scannedSourcesCount => _scannedSourcesCount; | |
| Duration get compileDuration => _compileDuration; | |
| Duration get transferDuration => _transferDuration; | |
| Duration get findInvalidatedDuration => _findInvalidatedDuration; | |
| /// Whether there was a hot reload rejection in this compile. | |
| /// | |
| /// On the web, hot reload can be rejected during compile time instead of at | |
| /// runtime. | |
| bool get hotReloadRejected => _hotReloadRejected; | |
| bool _success; | |
| int _invalidatedSourcesCount; | |
| int _syncedBytes; | |
| int _scannedSourcesCount; | |
| Duration _compileDuration; | |
| Duration _transferDuration; | |
| Duration _findInvalidatedDuration; | |
| bool _hotReloadRejected; | |
| void incorporateResults(UpdateFSReport report) { | |
| if (!report._success) { | |
| _success = false; | |
| } | |
| _invalidatedSourcesCount += report._invalidatedSourcesCount; | |
| _syncedBytes += report._syncedBytes; | |
| _scannedSourcesCount += report._scannedSourcesCount; | |
| _compileDuration += report._compileDuration; | |
| _transferDuration += report._transferDuration; | |
| _findInvalidatedDuration += report._findInvalidatedDuration; | |
| if (report._hotReloadRejected) { | |
| _hotReloadRejected = true; | |
| } | |
| } | |
| } | |
| class DevFS { | |
| /// Create a [DevFS] named [fsName] for the local files in [rootDirectory]. | |
| /// | |
| /// Failed uploads are retried after [uploadRetryThrottle] duration, defaults to 500ms. | |
| DevFS( | |
| FlutterVmService serviceProtocol, | |
| this.fsName, | |
| this.rootDirectory, { | |
| required OperatingSystemUtils osUtils, | |
| required Logger logger, | |
| required FileSystem fileSystem, | |
| required ProcessManager processManager, | |
| required Artifacts artifacts, | |
| required BuildMode buildMode, | |
| HttpClient? httpClient, | |
| Duration? uploadRetryThrottle, | |
| StopwatchFactory stopwatchFactory = const StopwatchFactory(), | |
| Config? config, | |
| }) : _vmService = serviceProtocol, | |
| _logger = logger, | |
| _fileSystem = fileSystem, | |
| _httpWriter = _DevFSHttpWriter( | |
| fsName, | |
| serviceProtocol, | |
| osUtils: osUtils, | |
| logger: logger, | |
| uploadRetryThrottle: uploadRetryThrottle, | |
| httpClient: | |
| httpClient ?? | |
| ((context.get<HttpClientFactory>() == null) | |
| ? HttpClient() | |
| : context.get<HttpClientFactory>()!()), | |
| ), | |
| _stopwatchFactory = stopwatchFactory, | |
| _config = config, | |
| _assetTransformer = DevelopmentAssetTransformer( | |
| transformer: AssetTransformer( | |
| processManager: processManager, | |
| fileSystem: fileSystem, | |
| dartBinaryPath: artifacts.getArtifactPath(Artifact.engineDartBinary), | |
| buildMode: buildMode, | |
| ), | |
| fileSystem: fileSystem, | |
| logger: logger, | |
| ); | |
| final FlutterVmService _vmService; | |
| final _DevFSHttpWriter _httpWriter; | |
| final Logger _logger; | |
| final FileSystem _fileSystem; | |
| final StopwatchFactory _stopwatchFactory; | |
| final Config? _config; | |
| final DevelopmentAssetTransformer _assetTransformer; | |
| final String fsName; | |
| final Directory rootDirectory; | |
| final assetPathsToEvict = <String>{}; | |
| final shaderPathsToEvict = <String>{}; | |
| // A flag to indicate whether we have called `setAssetDirectory` on the target device. | |
| bool hasSetAssetDirectory = false; | |
| /// Whether the font manifest was uploaded during [update]. | |
| bool didUpdateFontManifest = false; | |
| List<Uri> sources = <Uri>[]; | |
| DateTime? lastCompiled; | |
| DateTime? _previousCompiled; | |
| PackageConfig? lastPackageConfig; | |
| Uri? _baseUri; | |
| Uri? get baseUri => _baseUri; | |
| Uri deviceUriToHostUri(Uri deviceUri) { | |
| final deviceUriString = deviceUri.toString(); | |
| final baseUriString = baseUri.toString(); | |
| if (deviceUriString.startsWith(baseUriString)) { | |
| final String deviceUriSuffix = deviceUriString.substring(baseUriString.length); | |
| return rootDirectory.uri.resolve(deviceUriSuffix); | |
| } | |
| return deviceUri; | |
| } | |
| Future<Uri> create() async { | |
| _logger.printTrace('DevFS: Creating new filesystem on the device ($_baseUri)'); | |
| try { | |
| final vm_service.Response response = await _vmService.createDevFS(fsName); | |
| _baseUri = Uri.parse(response.json!['uri'] as String); | |
| } on vm_service.RPCError catch (rpcException) { | |
| if (rpcException.code == vm_service.RPCErrorKind.kServiceDisappeared.code || | |
| rpcException.code == vm_service.RPCErrorKind.kConnectionDisposed.code || | |
| rpcException.message.contains('Service connection disposed')) { | |
| // This can happen if the device has been disconnected, so translate to | |
| // a DevFSException, which the caller will handle. | |
| throw DevFSException('Service disconnected', rpcException); | |
| } | |
| // 1001 is kFileSystemAlreadyExists in //dart/runtime/vm/json_stream.h | |
| if (rpcException.code != 1001) { | |
| // Other RPCErrors are unexpected. Rethrow so it will hit crash | |
| // logging. | |
| rethrow; | |
| } | |
| _logger.printTrace('DevFS: Creating failed. Destroying and trying again'); | |
| await destroy(); | |
| final vm_service.Response response = await _vmService.createDevFS(fsName); | |
| _baseUri = Uri.parse(response.json!['uri'] as String); | |
| } | |
| _logger.printTrace('DevFS: Created new filesystem on the device ($_baseUri)'); | |
| return _baseUri!; | |
| } | |
| Future<void> destroy() async { | |
| _logger.printTrace('DevFS: Deleting filesystem on the device ($_baseUri)'); | |
| await _vmService.deleteDevFS(fsName); | |
| _logger.printTrace('DevFS: Deleted filesystem on the device ($_baseUri)'); | |
| } | |
| /// Mark the [lastCompiled] time to the previous successful compile. | |
| /// | |
| /// Sometimes a hot reload will be rejected by the VM due to a change in the | |
| /// structure of the code not supporting the hot reload. In these cases, | |
| /// the best resolution is a hot restart. However, the resident runner | |
| /// will not recognize this file as having been changed since the delta | |
| /// will already have been accepted. Instead, reset the compile time so | |
| /// that the last updated files are included in subsequent compilations until | |
| /// a reload is accepted. | |
| void resetLastCompiled() { | |
| lastCompiled = _previousCompiled; | |
| } | |
| /// Updates files on the device. | |
| /// | |
| /// Returns the number of bytes synced. | |
| /// | |
| /// If [fullRestart] is true, assumes this is a hot restart instead of a hot | |
| /// reload. If [resetCompiler] is true, sends a `reset` instruction to the | |
| /// frontend server. | |
| Future<UpdateFSReport> update({ | |
| required Uri mainUri, | |
| required ResidentCompiler generator, | |
| required bool trackWidgetCreation, | |
| required String pathToReload, | |
| required List<Uri> invalidatedFiles, | |
| required PackageConfig packageConfig, | |
| required String dillOutputPath, | |
| required DevelopmentShaderCompiler shaderCompiler, | |
| DevFSWriter? devFSWriter, | |
| String? target, | |
| AssetBundle? bundle, | |
| bool bundleFirstUpload = false, | |
| bool fullRestart = false, | |
| bool resetCompiler = false, | |
| File? dartPluginRegistrant, | |
| }) async { | |
| final candidateCompileTime = DateTime.now(); | |
| lastPackageConfig = packageConfig; | |
| // Update modified files | |
| final dirtyEntries = <Uri, DevFSContent>{}; | |
| var assetBuildFailed = false; | |
| var syncedBytes = 0; | |
| if (resetCompiler) { | |
| generator.reset(); | |
| } | |
| // On a full restart, or on an initial compile for the attach based workflow, | |
| // this will produce a full dill. Subsequent invocations will produce incremental | |
| // dill files that depend on the invalidated files. | |
| _logger.printTrace('Compiling dart to kernel with ${invalidatedFiles.length} updated files'); | |
| // Await the compiler response after checking if the bundle is updated. This allows the file | |
| // stating to be done while waiting for the frontend_server response. | |
| final Stopwatch compileTimer = _stopwatchFactory.createStopwatch('compile')..start(); | |
| final Future<CompilerOutput?> pendingCompilerOutput = generator | |
| .recompile( | |
| mainUri, | |
| invalidatedFiles, | |
| outputPath: dillOutputPath, | |
| fs: _fileSystem, | |
| projectRootPath: rootDirectory.path, | |
| packageConfig: packageConfig, | |
| checkDartPluginRegistry: true, // The entry point is assumed not to have changed. | |
| dartPluginRegistrant: dartPluginRegistrant, | |
| ) | |
| .then((CompilerOutput? result) { | |
| compileTimer.stop(); | |
| return result; | |
| }); | |
| if (bundle != null) { | |
| // Mark processing of bundle started for testability of starting the compile | |
| // before processing bundle. | |
| _logger.printTrace('Processing bundle.'); | |
| // await null to give time for telling the compiler to compile. | |
| await null; | |
| final String assetDirectory = getAssetBuildDirectory(_config, _fileSystem); | |
| try { | |
| final int bundleSyncedBytes = await updateBundle( | |
| bundle: bundle, | |
| dirtyEntries: dirtyEntries, | |
| assetDirectory: assetDirectory, | |
| assetTransformer: _assetTransformer, | |
| shaderCompiler: shaderCompiler, | |
| fileSystem: _fileSystem, | |
| rootDirectoryPath: rootDirectory.path, | |
| assetPathsToEvict: assetPathsToEvict, | |
| shaderPathsToEvict: shaderPathsToEvict, | |
| bundleFirstUpload: bundleFirstUpload, | |
| onFontManifestUpdated: () => didUpdateFontManifest = true, | |
| ); | |
| syncedBytes += bundleSyncedBytes; | |
| } on Exception catch (err, stackTrace) { | |
| _logger.printError('Error updating bundle: $err'); | |
| _logger.printTrace('$stackTrace'); | |
| assetBuildFailed = true; | |
| } | |
| // Mark processing of bundle done for testability of starting the compile | |
| // before processing bundle. | |
| _logger.printTrace('Bundle processing done.'); | |
| } | |
| final CompilerOutput? compilerOutput = await pendingCompilerOutput; | |
| if (compilerOutput == null || compilerOutput.errorCount > 0) { | |
| return UpdateFSReport(); | |
| } | |
| // Only update the last compiled time if we successfully compiled. | |
| _previousCompiled = lastCompiled; | |
| lastCompiled = candidateCompileTime; | |
| // list of sources that needs to be monitored are in [compilerOutput.sources] | |
| sources = compilerOutput.sources; | |
| // | |
| // Don't send full kernel file that would overwrite what VM already | |
| // started loading from. | |
| if (!bundleFirstUpload) { | |
| final String compiledBinary = compilerOutput.outputFilename; | |
| if (compiledBinary.isNotEmpty) { | |
| final Uri entryUri = _fileSystem.path.toUri(pathToReload); | |
| final content = DevFSFileContent(_fileSystem.file(compiledBinary)); | |
| syncedBytes += content.size; | |
| dirtyEntries[entryUri] = content; | |
| } | |
| } | |
| _logger.printTrace('Updating files.'); | |
| final Stopwatch transferTimer = _stopwatchFactory.createStopwatch('transfer')..start(); | |
| if (assetBuildFailed) { | |
| return UpdateFSReport(); | |
| } | |
| _logger.printTrace('Pending asset builds completed. Writing dirty entries.'); | |
| if (dirtyEntries.isNotEmpty) { | |
| await (devFSWriter ?? _httpWriter).write(dirtyEntries, _baseUri!, _httpWriter); | |
| } | |
| transferTimer.stop(); | |
| _logger.printTrace('DevFS: Sync finished'); | |
| return UpdateFSReport( | |
| success: true, | |
| syncedBytes: syncedBytes, | |
| invalidatedSourcesCount: invalidatedFiles.length, | |
| compileDuration: compileTimer.elapsed, | |
| transferDuration: transferTimer.elapsed, | |
| ); | |
| } | |
| /// Converts a platform-specific file path to a platform-independent URL path. | |
| static String _asUriPath(FileSystem fileSystem, String filePath) => | |
| '${fileSystem.path.toUri(filePath).path}/'; | |
| /// Process and sync [bundle] assets to [dirtyEntries]. | |
| /// | |
| /// Returns the total number of bytes processed. | |
| /// Throws an [Exception] if an asset build failed. | |
| static Future<int> updateBundle({ | |
| required AssetBundle bundle, | |
| required Map<Uri, DevFSContent> dirtyEntries, | |
| required String assetDirectory, | |
| required DevelopmentAssetTransformer assetTransformer, | |
| required DevelopmentShaderCompiler shaderCompiler, | |
| required FileSystem fileSystem, | |
| required String rootDirectoryPath, | |
| required Set<String> assetPathsToEvict, | |
| required Set<String> shaderPathsToEvict, | |
| required bool bundleFirstUpload, | |
| bool syncAllAssetsOnFirstUpload = false, | |
| void Function()? onFontManifestUpdated, | |
| }) async { | |
| if (bundleFirstUpload && !syncAllAssetsOnFirstUpload) { | |
| for (final AssetBundleEntry entry in bundle.entries.values) { | |
| entry.content.markClean(); | |
| } | |
| return 0; | |
| } | |
| final String assetBuildDirPrefix = _asUriPath(fileSystem, assetDirectory); | |
| final pendingAssetBuilds = <Future<void>>[]; | |
| var syncedBytes = 0; | |
| final syncedEntries = <AssetBundleEntry>[]; | |
| bundle.entries.forEach((String archivePath, AssetBundleEntry entry) { | |
| if (!bundleFirstUpload && !entry.content.isModified) { | |
| return; | |
| } | |
| syncedEntries.add(entry); | |
| // Modified shaders must be recompiled per-target platform. | |
| final Uri deviceUri = fileSystem.path.toUri( | |
| fileSystem.path.join(assetDirectory, archivePath), | |
| ); | |
| if (deviceUri.path.startsWith(assetBuildDirPrefix)) { | |
| archivePath = deviceUri.path.substring(assetBuildDirPrefix.length); | |
| } | |
| // If the font manifest is updated, invoke the callback so the hot runner | |
| // can invoke a service extension to force the engine to reload fonts. | |
| if (archivePath == kFontManifestJson) { | |
| onFontManifestUpdated?.call(); | |
| } | |
| switch (entry.kind) { | |
| case AssetKind.shader: | |
| pendingAssetBuilds.add(() async { | |
| DevFSContent content = entry.content; | |
| if (entry.transformers.isNotEmpty) { | |
| final DevFSContent? transformed = await assetTransformer.retransformAsset( | |
| inputAssetKey: archivePath, | |
| inputAssetContent: content, | |
| transformerEntries: entry.transformers, | |
| workingDirectory: rootDirectoryPath, | |
| ); | |
| if (transformed == null) { | |
| throw AssetTransformationException(archivePath, 'Failed to transform shader'); | |
| } | |
| content = transformed; | |
| } | |
| final DevFSContent? compiled = await shaderCompiler.recompileShader(content); | |
| if (compiled == null) { | |
| throw DevFSShaderCompilationException(archivePath, 'Failed to compile shader'); | |
| } | |
| dirtyEntries[deviceUri] = compiled; | |
| syncedBytes += compiled.size; | |
| if (!bundleFirstUpload) { | |
| shaderPathsToEvict.add(archivePath); | |
| } | |
| }()); | |
| case AssetKind.regular: | |
| case AssetKind.font: | |
| pendingAssetBuilds.add(() async { | |
| DevFSContent? content; | |
| if (entry.transformers.isEmpty || entry.kind != AssetKind.regular) { | |
| content = entry.content; | |
| } else { | |
| content = await assetTransformer.retransformAsset( | |
| inputAssetKey: archivePath, | |
| inputAssetContent: entry.content, | |
| transformerEntries: entry.transformers, | |
| workingDirectory: rootDirectoryPath, | |
| ); | |
| } | |
| if (content == null) { | |
| throw AssetTransformationException(archivePath, 'Failed to transform asset'); | |
| } | |
| dirtyEntries[deviceUri] = content; | |
| syncedBytes += content.size; | |
| if (!bundleFirstUpload) { | |
| assetPathsToEvict.add(archivePath); | |
| } | |
| }()); | |
| } | |
| }); | |
| await Future.wait(pendingAssetBuilds); | |
| for (final entry in syncedEntries) { | |
| entry.content.markClean(); | |
| } | |
| return syncedBytes; | |
| } | |
| } | |
| /// An implementation of a devFS writer which copies physical files for devices | |
| /// running on the same host. | |
| /// | |
| /// DevFS entries which correspond to physical files are copied using [File.copySync], | |
| /// while entries that correspond to arbitrary string/byte values are written from | |
| /// memory. | |
| /// | |
| /// Requires that the file system is the same for both the tool and application. | |
| class LocalDevFSWriter implements DevFSWriter { | |
| LocalDevFSWriter({required FileSystem fileSystem}) : _fileSystem = fileSystem; | |
| final FileSystem _fileSystem; | |
| @override | |
| Future<void> write(Map<Uri, DevFSContent> entries, Uri baseUri, [DevFSWriter? parent]) async { | |
| try { | |
| for (final MapEntry<Uri, DevFSContent> entry in entries.entries) { | |
| final Uri uri = entry.key; | |
| final DevFSContent devFSContent = entry.value; | |
| final File destination = _fileSystem.file(baseUri.resolveUri(uri)); | |
| if (!destination.parent.existsSync()) { | |
| destination.parent.createSync(recursive: true); | |
| } | |
| if (devFSContent is DevFSFileContent) { | |
| final content = devFSContent.file as File; | |
| content.copySync(destination.path); | |
| continue; | |
| } | |
| destination.writeAsBytesSync(await devFSContent.contentsAsBytes()); | |
| } | |
| } on FileSystemException catch (err) { | |
| throw DevFSException(err.toString()); | |
| } | |
| } | |
| } | |
| /// Exception thrown when development-time asset transformation fails. | |
| final class AssetTransformationException implements Exception { | |
| AssetTransformationException(this.archivePath, this.message); | |
| final String archivePath; | |
| final String message; | |
| @override | |
| String toString() => 'AssetTransformationException: $message (Asset: $archivePath)'; | |
| } | |
| /// Exception thrown when development-time shader compilation fails. | |
| final class DevFSShaderCompilationException implements Exception { | |
| DevFSShaderCompilationException(this.archivePath, this.message); | |
| final String archivePath; | |
| final String message; | |
| @override | |
| String toString() => 'DevFSShaderCompilationException: $message (Shader: $archivePath)'; | |
| } |