devfs.dart 26.8 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
6

7
import 'package:package_config/package_config.dart';
8
import 'package:vm_service/vm_service.dart' as vm_service;
9

10
import 'asset.dart';
11
import 'base/context.dart';
12
import 'base/file_system.dart';
13
import 'base/io.dart';
14
import 'base/logger.dart';
15
import 'base/net.dart';
16
import 'base/os.dart';
17
import 'build_info.dart';
18
import 'build_system/targets/scene_importer.dart';
19
import 'build_system/targets/shader_compiler.dart';
20
import 'compile.dart';
21
import 'convert.dart' show base64, utf8;
22
import 'vmservice.dart';
23

24 25
const String _kFontManifest = 'FontManifest.json';

26 27 28
class DevFSConfig {
  /// Should DevFS assume that symlink targets are stable?
  bool cacheSymlinks = false;
29 30
  /// Should DevFS assume that there are no symlinks to directories?
  bool noDirectorySymlinks = false;
31 32
}

33
DevFSConfig? get devFSConfig => context.get<DevFSConfig>();
34

35 36
/// Common superclass for content copied to the device.
abstract class DevFSContent {
37
  /// Return true if this is the first time this method is called
38 39
  /// or if the entry has been modified since this method was last called.
  bool get isModified;
40

41 42 43 44 45
  /// Return true if this is the first time this method is called
  /// or if the entry has been modified after the given time
  /// or if the given time is null.
  bool isModifiedAfter(DateTime time);

46 47
  int get size;

48
  Future<List<int>> contentsAsBytes();
49

50 51
  Stream<List<int>> contentsAsStream();

52 53 54 55
  Stream<List<int>> contentsAsCompressedStream(
    OperatingSystemUtils osUtils,
  ) {
    return osUtils.gzipLevel1Stream(contentsAsStream());
56 57 58
  }
}

59
// File content to be copied to the device.
60 61
class DevFSFileContent extends DevFSContent {
  DevFSFileContent(this.file);
62

63
  final FileSystemEntity file;
64 65
  File? _linkTarget;
  FileStat? _fileStat;
66

67
  File _getFile() {
68
    final File? linkTarget = _linkTarget;
69 70
    if (linkTarget != null) {
      return linkTarget;
71
    }
72 73
    if (file is Link) {
      // The link target.
74
      return file.fileSystem.file(file.resolveSymbolicLinksSync());
75
    }
76
    return file as File;
77 78
  }

79
  void _stat() {
80
    final File? linkTarget = _linkTarget;
81
    if (linkTarget != null) {
82
      // Stat the cached symlink target.
83
      final FileStat fileStat = linkTarget.statSync();
84 85 86 87 88 89
      if (fileStat.type == FileSystemEntityType.notFound) {
        _linkTarget = null;
      } else {
        _fileStat = fileStat;
        return;
      }
90
    }
91 92
    final FileStat fileStat = file.statSync();
    _fileStat = fileStat.type == FileSystemEntityType.notFound ? null : fileStat;
93
    if (_fileStat != null && _fileStat?.type == FileSystemEntityType.link) {
94
      // Resolve, stat, and maybe cache the symlink target.
95
      final String resolved = file.resolveSymbolicLinksSync();
96
      final File linkTarget = file.fileSystem.file(resolved);
97
      // Stat the link target.
98 99 100 101
      final FileStat fileStat = linkTarget.statSync();
      if (fileStat.type == FileSystemEntityType.notFound) {
        _fileStat = null;
        _linkTarget = null;
102
      } else if (devFSConfig?.cacheSymlinks ?? false) {
103 104
        _linkTarget = linkTarget;
      }
105
    }
106
  }
107

108 109
  @override
  bool get isModified {
110
    final FileStat? oldFileStat = _fileStat;
111
    _stat();
112
    final FileStat? newFileStat = _fileStat;
113
    if (oldFileStat == null && newFileStat == null) {
114
      return false;
115
    }
116
    return oldFileStat == null || newFileStat == null || newFileStat.modified.isAfter(oldFileStat.modified);
117 118
  }

119 120
  @override
  bool isModifiedAfter(DateTime time) {
121
    final FileStat? oldFileStat = _fileStat;
122
    _stat();
123
    final FileStat? newFileStat = _fileStat;
124
    if (oldFileStat == null && newFileStat == null) {
125
      return false;
126
    }
127
    return oldFileStat == null
128 129
        || newFileStat == null
        || newFileStat.modified.isAfter(time);
130 131
  }

132 133
  @override
  int get size {
134
    if (_fileStat == null) {
135
      _stat();
136
    }
137 138
    // Can still be null if the file wasn't found.
    return _fileStat?.size ?? 0;
139
  }
140

141
  @override
142
  Future<List<int>> contentsAsBytes() async => _getFile().readAsBytes();
143 144

  @override
145
  Stream<List<int>> contentsAsStream() => _getFile().openRead();
146 147 148 149 150 151 152 153 154
}

/// Byte content to be copied to the device.
class DevFSByteContent extends DevFSContent {
  DevFSByteContent(this._bytes);

  List<int> _bytes;

  bool _isModified = true;
155
  DateTime _modificationTime = DateTime.now();
156 157 158

  List<int> get bytes => _bytes;

159 160
  set bytes(List<int> value) {
    _bytes = value;
161
    _isModified = true;
162
    _modificationTime = DateTime.now();
163 164
  }

165
  /// Return true only once so that the content is written to the device only once.
166 167
  @override
  bool get isModified {
168
    final bool modified = _isModified;
169 170
    _isModified = false;
    return modified;
171
  }
172

173 174
  @override
  bool isModifiedAfter(DateTime time) {
175
    return _modificationTime.isAfter(time);
176 177
  }

178 179 180 181
  @override
  int get size => _bytes.length;

  @override
182
  Future<List<int>> contentsAsBytes() async => _bytes;
183 184

  @override
185 186
  Stream<List<int>> contentsAsStream() =>
      Stream<List<int>>.fromIterable(<List<int>>[_bytes]);
187 188
}

189
/// String content to be copied to the device.
190
class DevFSStringContent extends DevFSByteContent {
191 192 193
  DevFSStringContent(String string)
    : _string = string,
      super(utf8.encode(string));
194 195 196 197 198

  String _string;

  String get string => _string;

199 200
  set string(String value) {
    _string = value;
201
    super.bytes = utf8.encode(_string);
202 203 204
  }

  @override
205
  set bytes(List<int> value) {
206
    string = utf8.decode(value);
207 208
  }
}
209

210 211 212 213 214 215 216 217 218 219 220
/// 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 {
221
  DevFSStringCompressingBytesContent(this._string, { String? hintString })
222 223 224 225 226 227 228 229 230 231 232 233 234 235
    : _compressor = ZLibEncoder(
      dictionary: hintString == null
          ? null
          : utf8.encode(hintString),
      gzip: true,
      level: 9,
    );

  final String _string;
  final ZLibEncoder _compressor;
  final DateTime _modificationTime = DateTime.now();

  bool _isModified = true;

236
  late final List<int> bytes = _compressor.convert(utf8.encode(_string));
237 238 239 240 241 242 243 244 245 246 247

  /// Return true only once so that the content is written to the device only once.
  @override
  bool get isModified {
    final bool modified = _isModified;
    _isModified = false;
    return modified;
  }

  @override
  bool isModifiedAfter(DateTime time) {
248
    return _modificationTime.isAfter(time);
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263
  }

  @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;
}

264 265 266 267
class DevFSException implements Exception {
  DevFSException(this.message, [this.error, this.stackTrace]);
  final String message;
  final dynamic error;
268
  final StackTrace? stackTrace;
269 270 271

  @override
  String toString() => 'DevFSException($message, $error, $stackTrace)';
272 273
}

274 275 276 277 278 279 280 281 282 283 284
/// 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 {
285 286
  _DevFSHttpWriter(
    this.fsName,
287
    FlutterVmService serviceProtocol, {
288 289 290 291
    required OperatingSystemUtils osUtils,
    required HttpClient httpClient,
    required Logger logger,
    Duration? uploadRetryThrottle,
292
  })
293
    : httpAddress = serviceProtocol.httpAddress,
294 295
      _client = httpClient,
      _osUtils = osUtils,
296
      _uploadRetryThrottle = uploadRetryThrottle,
297
      _logger = logger;
298

299
  final HttpClient _client;
300
  final OperatingSystemUtils _osUtils;
301
  final Logger _logger;
302
  final Duration? _uploadRetryThrottle;
303 304

  final String fsName;
305
  final Uri? httpAddress;
306

307
  // 3 was chosen to try to limit the variance in the time it takes to execute
308 309 310 311
  // `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 int kMaxInFlight = 3;
312 313

  int _inFlight = 0;
314 315
  late Map<Uri, DevFSContent> _outstanding;
  late Completer<void> _completer;
316

317
  @override
318
  Future<void> write(Map<Uri, DevFSContent> entries, Uri devFSBase, [DevFSWriter? parent]) async {
319 320 321 322 323 324 325 326 327 328 329 330 331
    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);
    }
332 333
  }

334
  void _scheduleWrites() {
335
    while ((_inFlight < kMaxInFlight) && (!_completer.isCompleted) && _outstanding.isNotEmpty) {
336
      final Uri deviceUri = _outstanding.keys.first;
337
      final DevFSContent content = _outstanding.remove(deviceUri)!;
338
      _startWrite(deviceUri, content, retry: 10);
339
      _inFlight += 1;
340
    }
341
    if ((_inFlight == 0) && (!_completer.isCompleted) && _outstanding.isEmpty) {
342
      _completer.complete();
343
    }
344 345
  }

346
  Future<void> _startWrite(
347
    Uri deviceUri,
348
    DevFSContent content, {
349
    int retry = 0,
350 351 352
  }) async {
    while(true) {
      try {
353
        final HttpClientRequest request = await _client.putUrl(httpAddress!);
354 355 356
        request.headers.removeAll(HttpHeaders.acceptEncodingHeader);
        request.headers.add('dev_fs_name', fsName);
        request.headers.add('dev_fs_uri_b64', base64.encode(utf8.encode('$deviceUri')));
357 358 359
        final Stream<List<int>> contents = content.contentsAsCompressedStream(
          _osUtils,
        );
360
        await request.addStream(contents);
361
        // Once the bug in Dart is solved we can remove the timeout
362
        // (https://github.com/dart-lang/sdk/issues/43525).
363 364
        try {
          final HttpClientResponse response = await request.close().timeout(
365
            const Duration(seconds: 60));
366 367 368 369 370 371 372 373 374 375 376 377 378
          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;
        }
379
        break;
380
      } on Exception catch (error, trace) {
381
        if (!_completer.isCompleted) {
382
          _logger.printTrace('Error writing "$deviceUri" to DevFS: $error');
383 384
          if (retry > 0) {
            retry--;
385
            _logger.printTrace('trying again in a few - $retry more attempts left');
386
            await Future<void>.delayed(_uploadRetryThrottle ?? const Duration(milliseconds: 500));
387 388 389 390
            continue;
          }
          _completer.completeError(error, trace);
        }
391
      }
Ryan Macnak's avatar
Ryan Macnak committed
392
    }
393 394
    _inFlight -= 1;
    _scheduleWrites();
395 396 397
  }
}

398 399
// Basic statistics for DevFS update operation.
class UpdateFSReport {
400 401 402 403
  UpdateFSReport({
    bool success = false,
    int invalidatedSourcesCount = 0,
    int syncedBytes = 0,
404
    this.fastReassembleClassName,
405 406 407 408
    int scannedSourcesCount = 0,
    Duration compileDuration = Duration.zero,
    Duration transferDuration = Duration.zero,
    Duration findInvalidatedDuration = Duration.zero,
409 410
  }) : _success = success,
       _invalidatedSourcesCount = invalidatedSourcesCount,
411 412 413 414 415
       _syncedBytes = syncedBytes,
       _scannedSourcesCount = scannedSourcesCount,
       _compileDuration = compileDuration,
       _transferDuration = transferDuration,
       _findInvalidatedDuration = findInvalidatedDuration;
416 417 418 419

  bool get success => _success;
  int get invalidatedSourcesCount => _invalidatedSourcesCount;
  int get syncedBytes => _syncedBytes;
420 421 422 423
  int get scannedSourcesCount => _scannedSourcesCount;
  Duration get compileDuration => _compileDuration;
  Duration get transferDuration => _transferDuration;
  Duration get findInvalidatedDuration => _findInvalidatedDuration;
424

425
  bool _success;
426
  String? fastReassembleClassName;
427 428
  int _invalidatedSourcesCount;
  int _syncedBytes;
429 430 431 432
  int _scannedSourcesCount;
  Duration _compileDuration;
  Duration _transferDuration;
  Duration _findInvalidatedDuration;
433

434 435 436 437
  void incorporateResults(UpdateFSReport report) {
    if (!report._success) {
      _success = false;
    }
438
    fastReassembleClassName ??= report.fastReassembleClassName;
439 440
    _invalidatedSourcesCount += report._invalidatedSourcesCount;
    _syncedBytes += report._syncedBytes;
441 442 443 444
    _scannedSourcesCount += report._scannedSourcesCount;
    _compileDuration += report._compileDuration;
    _transferDuration += report._transferDuration;
    _findInvalidatedDuration += report._findInvalidatedDuration;
445 446 447
  }
}

448
class DevFS {
449
  /// Create a [DevFS] named [fsName] for the local files in [rootDirectory].
450 451
  ///
  /// Failed uploads are retried after [uploadRetryThrottle] duration, defaults to 500ms.
452
  DevFS(
453
    FlutterVmService serviceProtocol,
454 455
    this.fsName,
    this.rootDirectory, {
456 457 458 459 460
    required OperatingSystemUtils osUtils,
    required Logger logger,
    required FileSystem fileSystem,
    HttpClient? httpClient,
    Duration? uploadRetryThrottle,
461
    StopwatchFactory stopwatchFactory = const StopwatchFactory(),
462 463 464
  }) : _vmService = serviceProtocol,
       _logger = logger,
       _fileSystem = fileSystem,
465 466 467 468
       _httpWriter = _DevFSHttpWriter(
        fsName,
        serviceProtocol,
        osUtils: osUtils,
469
        logger: logger,
470
        uploadRetryThrottle: uploadRetryThrottle,
471 472
        httpClient: httpClient ?? ((context.get<HttpClientFactory>() == null)
          ? HttpClient()
473
          : context.get<HttpClientFactory>()!())),
474
       _stopwatchFactory = stopwatchFactory;
475

476
  final FlutterVmService _vmService;
477
  final _DevFSHttpWriter _httpWriter;
478 479
  final Logger _logger;
  final FileSystem _fileSystem;
480
  final StopwatchFactory _stopwatchFactory;
481

482
  final String fsName;
483
  final Directory? rootDirectory;
484
  final Set<String> assetPathsToEvict = <String>{};
485
  final Set<String> shaderPathsToEvict = <String>{};
486
  final Set<String> scenePathsToEvict = <String>{};
487

488 489 490
  // A flag to indicate whether we have called `setAssetDirectory` on the target device.
  bool hasSetAssetDirectory = false;

491 492 493
  /// Whether the font manifest was uploaded during [update].
  bool didUpdateFontManifest = false;

494
  List<Uri> sources = <Uri>[];
495 496 497 498
  DateTime? lastCompiled;
  DateTime? _previousCompiled;
  PackageConfig? lastPackageConfig;
  File? _widgetCacheOutputFile;
499

500 501
  Uri? _baseUri;
  Uri? get baseUri => _baseUri;
502

503 504 505 506 507
  Uri deviceUriToHostUri(Uri deviceUri) {
    final String deviceUriString = deviceUri.toString();
    final String baseUriString = baseUri.toString();
    if (deviceUriString.startsWith(baseUriString)) {
      final String deviceUriSuffix = deviceUriString.substring(baseUriString.length);
508
      return rootDirectory!.uri.resolve(deviceUriSuffix);
509 510 511 512
    }
    return deviceUri;
  }

513
  Future<Uri> create() async {
514
    _logger.printTrace('DevFS: Creating new filesystem on the device ($_baseUri)');
515
    try {
516
      final vm_service.Response response = await _vmService.createDevFS(fsName);
517
      _baseUri = Uri.parse(response.json!['uri'] as String);
518
    } on vm_service.RPCError catch (rpcException) {
519 520 521 522 523
      if (rpcException.code == RPCErrorCodes.kServiceDisappeared) {
        // This can happen if the device has been disconnected, so translate to
        // a DevFSException, which the caller will handle.
        throw DevFSException('Service disconnected', rpcException);
      }
524
      // 1001 is kFileSystemAlreadyExists in //dart/runtime/vm/json_stream.h
525
      if (rpcException.code != 1001) {
526 527
        // Other RPCErrors are unexpected. Rethrow so it will hit crash
        // logging.
528
        rethrow;
529
      }
530
      _logger.printTrace('DevFS: Creating failed. Destroying and trying again');
531
      await destroy();
532
      final vm_service.Response response = await _vmService.createDevFS(fsName);
533
      _baseUri = Uri.parse(response.json!['uri'] as String);
534
    }
535
    _logger.printTrace('DevFS: Created new filesystem on the device ($_baseUri)');
536
    return _baseUri!;
537 538
  }

539
  Future<void> destroy() async {
540 541 542
    _logger.printTrace('DevFS: Deleting filesystem on the device ($_baseUri)');
    await _vmService.deleteDevFS(fsName);
    _logger.printTrace('DevFS: Deleted filesystem on the device ($_baseUri)');
543 544
  }

545 546 547 548 549 550 551 552 553 554 555 556 557 558
  /// 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;
  }


559 560 561 562
  /// If the build method of a single widget was modified, return the widget name.
  ///
  /// If any other changes were made, or there is an error scanning the file,
  /// return `null`.
563 564
  String? _checkIfSingleWidgetReloadApplied() {
    final File? widgetCacheOutputFile = _widgetCacheOutputFile;
565 566
    if (widgetCacheOutputFile != null && widgetCacheOutputFile.existsSync()) {
      final String widget = widgetCacheOutputFile.readAsStringSync().trim();
567 568 569 570 571 572 573
      if (widget.isNotEmpty) {
        return widget;
      }
    }
    return null;
  }

574 575 576
  /// Updates files on the device.
  ///
  /// Returns the number of bytes synced.
577
  Future<UpdateFSReport> update({
578 579 580 581 582 583 584
    required Uri mainUri,
    required ResidentCompiler generator,
    required bool trackWidgetCreation,
    required String pathToReload,
    required List<Uri> invalidatedFiles,
    required PackageConfig packageConfig,
    required String dillOutputPath,
585
    required DevelopmentShaderCompiler shaderCompiler,
586
    DevelopmentSceneImporter? sceneImporter,
587 588 589 590
    DevFSWriter? devFSWriter,
    String? target,
    AssetBundle? bundle,
    DateTime? firstBuildTime,
591 592
    bool bundleFirstUpload = false,
    bool fullRestart = false,
593
    String? projectRootPath,
594
    File? dartPluginRegistrant,
595
  }) async {
596
    final DateTime candidateCompileTime = DateTime.now();
597
    didUpdateFontManifest = false;
598
    lastPackageConfig = packageConfig;
599
    _widgetCacheOutputFile = _fileSystem.file('$dillOutputPath.incremental.dill.widget_cache');
600

601 602
    // Update modified files
    final Map<Uri, DevFSContent> dirtyEntries = <Uri, DevFSContent>{};
603 604
    final List<Future<void>> pendingAssetBuilds = <Future<void>>[];
    bool assetBuildFailed = false;
605
    int syncedBytes = 0;
606 607 608 609 610 611 612 613 614 615
    if (fullRestart) {
      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.
616
    final Stopwatch compileTimer = _stopwatchFactory.createStopwatch('compile')..start();
617
    final Future<CompilerOutput?> pendingCompilerOutput = generator.recompile(
618 619
      mainUri,
      invalidatedFiles,
620
      outputPath: dillOutputPath,
621 622
      fs: _fileSystem,
      projectRootPath: projectRootPath,
623
      packageConfig: packageConfig,
624
      checkDartPluginRegistry: true, // The entry point is assumed not to have changed.
625
      dartPluginRegistrant: dartPluginRegistrant,
626
    ).then((CompilerOutput? result) {
627 628 629 630
      compileTimer.stop();
      return result;
    });

631
    if (bundle != null) {
632 633 634 635 636 637
      // 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;

638
      // The tool writes the assets into the AssetBundle working dir so that they
639
      // are in the same location in DevFS and the iOS simulator.
640
      final String assetBuildDirPrefix = _asUriPath(getAssetBuildDirectory());
641
      final String assetDirectory = getAssetBuildDirectory();
642
      bundle.entries.forEach((String archivePath, DevFSContent content) {
643 644
        // If the content is backed by a real file, isModified will file stat and return true if
        // it was modified since the last time this was called.
645 646 647
        if (!content.isModified || bundleFirstUpload) {
          return;
        }
648
        // Modified shaders must be recompiled per-target platform.
649
        final Uri deviceUri = _fileSystem.path.toUri(_fileSystem.path.join(assetDirectory, archivePath));
650 651 652
        if (deviceUri.path.startsWith(assetBuildDirPrefix)) {
          archivePath = deviceUri.path.substring(assetBuildDirPrefix.length);
        }
653 654 655 656 657
        // If the font manifest is updated, mark this as true so the hot runner
        // can invoke a service extension to force the engine to reload fonts.
        if (archivePath == _kFontManifest) {
          didUpdateFontManifest = true;
        }
658

659 660 661 662 663 664 665 666 667 668 669
        switch (bundle.entryKinds[archivePath]) {
          case AssetKind.shader:
            final Future<DevFSContent?> pending = shaderCompiler.recompileShader(content);
            pendingAssetBuilds.add(pending);
            pending.then((DevFSContent? content) {
              if (content == null) {
                assetBuildFailed = true;
                return;
              }
              dirtyEntries[deviceUri] = content;
              syncedBytes += content.size;
670
              if (!bundleFirstUpload) {
671 672 673 674 675 676 677
                shaderPathsToEvict.add(archivePath);
              }
            });
            break;
          case AssetKind.model:
            if (sceneImporter == null) {
              break;
678
            }
679 680 681 682 683 684 685 686 687
            final Future<DevFSContent?> pending = sceneImporter.reimportScene(content);
            pendingAssetBuilds.add(pending);
            pending.then((DevFSContent? content) {
              if (content == null) {
                assetBuildFailed = true;
                return;
              }
              dirtyEntries[deviceUri] = content;
              syncedBytes += content.size;
688
              if (!bundleFirstUpload) {
689 690 691 692 693 694 695
                scenePathsToEvict.add(archivePath);
              }
            });
            break;
          case AssetKind.regular:
          case AssetKind.font:
          case null:
696 697
            dirtyEntries[deviceUri] = content;
            syncedBytes += content.size;
698
            if (!bundleFirstUpload) {
699
              assetPathsToEvict.add(archivePath);
700
            }
701
        }
702
      });
703 704 705 706

      // Mark processing of bundle done for testability of starting the compile
      // before processing bundle.
      _logger.printTrace('Bundle processing done.');
707
    }
708
    final CompilerOutput? compilerOutput = await pendingCompilerOutput;
709
    if (compilerOutput == null || compilerOutput.errorCount > 0) {
710
      return UpdateFSReport();
711
    }
712
    // Only update the last compiled time if we successfully compiled.
713
    _previousCompiled = lastCompiled;
714
    lastCompiled = candidateCompileTime;
715
    // list of sources that needs to be monitored are in [compilerOutput.sources]
716
    sources = compilerOutput.sources;
717
    //
718 719 720
    // Don't send full kernel file that would overwrite what VM already
    // started loading from.
    if (!bundleFirstUpload) {
721 722
      final String compiledBinary = compilerOutput.outputFilename;
      if (compiledBinary.isNotEmpty) {
723
        final Uri entryUri = _fileSystem.path.toUri(pathToReload);
724
        final DevFSFileContent content = DevFSFileContent(_fileSystem.file(compiledBinary));
725 726
        syncedBytes += content.size;
        dirtyEntries[entryUri] = content;
727
      }
728
    }
729
    _logger.printTrace('Updating files.');
730
    final Stopwatch transferTimer = _stopwatchFactory.createStopwatch('transfer')..start();
731

732 733
    await Future.wait(pendingAssetBuilds);
    if (assetBuildFailed) {
734 735 736
      return UpdateFSReport();
    }

737
    if (dirtyEntries.isNotEmpty) {
738
      await (devFSWriter ?? _httpWriter).write(dirtyEntries, _baseUri!, _httpWriter);
739
    }
740
    transferTimer.stop();
741
    _logger.printTrace('DevFS: Sync finished');
742 743 744 745 746
    return UpdateFSReport(
      success: true,
      syncedBytes: syncedBytes,
      invalidatedSourcesCount: invalidatedFiles.length,
      fastReassembleClassName: _checkIfSingleWidgetReloadApplied(),
747 748
      compileDuration: compileTimer.elapsed,
      transferDuration: transferTimer.elapsed,
749
    );
750
  }
751

752
  /// Converts a platform-specific file path to a platform-independent URL path.
753
  String _asUriPath(String filePath) => '${_fileSystem.path.toUri(filePath).path}/';
754
}
755 756 757 758 759 760 761 762 763 764 765

/// 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({
766
    required FileSystem fileSystem,
767 768 769 770 771
  }) : _fileSystem = fileSystem;

  final FileSystem _fileSystem;

  @override
772
  Future<void> write(Map<Uri, DevFSContent> entries, Uri baseUri, [DevFSWriter? parent]) async {
773
    try {
774 775 776
      for (final MapEntry<Uri, DevFSContent> entry in entries.entries) {
        final Uri uri = entry.key;
        final DevFSContent devFSContent = entry.value;
777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792
        final File destination = _fileSystem.file(baseUri.resolveUri(uri));
        if (!destination.parent.existsSync()) {
          destination.parent.createSync(recursive: true);
        }
        if (devFSContent is DevFSFileContent) {
          final File content = devFSContent.file as File;
          content.copySync(destination.path);
          continue;
        }
        destination.writeAsBytesSync(await devFSContent.contentsAsBytes());
      }
    } on FileSystemException catch (err) {
      throw DevFSException(err.toString());
    }
  }
}