devfs.dart 25.6 KB
Newer Older
1 2 3 4 5
// 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';
6
import 'dart:convert' show base64, utf8;
7

8 9
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;

10
import 'asset.dart';
11
import 'base/context.dart';
12
import 'base/file_system.dart';
13
import 'base/io.dart';
14
import 'build_info.dart';
15
import 'compile.dart';
16 17
import 'dart/package_map.dart';
import 'globals.dart';
18
import 'vmservice.dart';
19

20 21 22
class DevFSConfig {
  /// Should DevFS assume that symlink targets are stable?
  bool cacheSymlinks = false;
23 24
  /// Should DevFS assume that there are no symlinks to directories?
  bool noDirectorySymlinks = false;
25 26 27 28
}

DevFSConfig get devFSConfig => context[DevFSConfig];

29 30 31
/// Common superclass for content copied to the device.
abstract class DevFSContent {
  bool _exists = true;
32

33
  /// Return true if this is the first time this method is called
34 35
  /// or if the entry has been modified since this method was last called.
  bool get isModified;
36

37 38 39 40 41
  /// 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);

42 43 44 45 46 47 48
  int get size;

  Future<List<int>> contentsAsBytes();

  Stream<List<int>> contentsAsStream();

  Stream<List<int>> contentsAsCompressedStream() {
49
    return contentsAsStream().transform(GZIP.encoder); // ignore: deprecated_member_use
50
  }
51 52 53

  /// Return the list of files this content depends on.
  List<String> get fileDependencies => <String>[];
54 55 56 57 58
}

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

60 61 62 63 64 65 66
  static DevFSFileContent clone(DevFSFileContent fsFileContent) {
    final DevFSFileContent newFsFileContent = new DevFSFileContent(fsFileContent.file);
    newFsFileContent._linkTarget = fsFileContent._linkTarget;
    newFsFileContent._fileStat = fsFileContent._fileStat;
    return newFsFileContent;
  }

67
  final FileSystemEntity file;
68
  FileSystemEntity _linkTarget;
69
  FileStat _fileStat;
70

71 72 73
  File _getFile() {
    if (_linkTarget != null) {
      return _linkTarget;
74
    }
75 76 77
    if (file is Link) {
      // The link target.
      return fs.file(file.resolveSymbolicLinksSync());
78
    }
79
    return file;
80 81
  }

82
  void _stat() {
83 84 85 86 87
    if (_linkTarget != null) {
      // Stat the cached symlink target.
      _fileStat = _linkTarget.statSync();
      return;
    }
88
    _fileStat = file.statSync();
89
    if (_fileStat.type == FileSystemEntityType.LINK) { // ignore: deprecated_member_use
90
      // Resolve, stat, and maybe cache the symlink target.
91 92
      final String resolved = file.resolveSymbolicLinksSync();
      final FileSystemEntity linkTarget = fs.file(resolved);
93 94 95 96 97
      // Stat the link target.
      _fileStat = linkTarget.statSync();
      if (devFSConfig.cacheSymlinks) {
        _linkTarget = linkTarget;
      }
98
    }
99
  }
100

101 102 103
  @override
  List<String> get fileDependencies => <String>[_getFile().path];

104 105
  @override
  bool get isModified {
106
    final FileStat _oldFileStat = _fileStat;
107 108
    _stat();
    return _oldFileStat == null || _fileStat.modified.isAfter(_oldFileStat.modified);
109 110
  }

111 112 113 114 115 116 117
  @override
  bool isModifiedAfter(DateTime time) {
    final FileStat _oldFileStat = _fileStat;
    _stat();
    return _oldFileStat == null || time == null || _fileStat.modified.isAfter(time);
  }

118 119 120 121 122
  @override
  int get size {
    if (_fileStat == null)
      _stat();
    return _fileStat.size;
123
  }
124

125 126 127 128 129 130 131 132 133 134 135 136 137 138
  @override
  Future<List<int>> contentsAsBytes() => _getFile().readAsBytes();

  @override
  Stream<List<int>> contentsAsStream() => _getFile().openRead();
}

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

  List<int> _bytes;

  bool _isModified = true;
139
  DateTime _modificationTime = new DateTime.now();
140 141 142

  List<int> get bytes => _bytes;

143 144
  set bytes(List<int> value) {
    _bytes = value;
145
    _isModified = true;
146
    _modificationTime = new DateTime.now();
147 148
  }

149
  /// Return true only once so that the content is written to the device only once.
150 151
  @override
  bool get isModified {
152
    final bool modified = _isModified;
153 154
    _isModified = false;
    return modified;
155
  }
156

157 158 159 160 161
  @override
  bool isModifiedAfter(DateTime time) {
    return time == null || _modificationTime.isAfter(time);
  }

162 163 164 165 166 167 168 169 170
  @override
  int get size => _bytes.length;

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

  @override
  Stream<List<int>> contentsAsStream() =>
      new Stream<List<int>>.fromIterable(<List<int>>[_bytes]);
171 172
}

173 174
/// String content to be copied to the device.
class DevFSStringContent extends DevFSByteContent {
175
  DevFSStringContent(String string) : _string = string, super(utf8.encode(string));
176 177 178 179 180

  String _string;

  String get string => _string;

181 182
  set string(String value) {
    _string = value;
183
    super.bytes = utf8.encode(_string);
184 185 186
  }

  @override
187
  set bytes(List<int> value) {
188
    string = utf8.decode(value);
189 190
  }
}
191 192 193 194 195

/// Abstract DevFS operations interface.
abstract class DevFSOperations {
  Future<Uri> create(String fsName);
  Future<dynamic> destroy(String fsName);
196 197
  Future<dynamic> writeFile(String fsName, Uri deviceUri, DevFSContent content);
  Future<dynamic> deleteFile(String fsName, Uri deviceUri);
198 199 200
}

/// An implementation of [DevFSOperations] that speaks to the
201
/// vm service.
202
class ServiceProtocolDevFSOperations implements DevFSOperations {
203
  final VMService vmService;
204

205
  ServiceProtocolDevFSOperations(this.vmService);
206 207 208

  @override
  Future<Uri> create(String fsName) async {
209
    final Map<String, dynamic> response = await vmService.vm.createDevFS(fsName);
210 211 212 213 214
    return Uri.parse(response['uri']);
  }

  @override
  Future<dynamic> destroy(String fsName) async {
215
    await vmService.vm.deleteDevFS(fsName);
216 217 218
  }

  @override
219
  Future<dynamic> writeFile(String fsName, Uri deviceUri, DevFSContent content) async {
220 221
    List<int> bytes;
    try {
222
      bytes = await content.contentsAsBytes();
223 224 225
    } catch (e) {
      return e;
    }
226
    final String fileContents = base64.encode(bytes);
227
    try {
228 229 230 231
      return await vmService.vm.invokeRpcRaw(
        '_writeDevFSFile',
        params: <String, dynamic> {
          'fsName': fsName,
232
          'uri': deviceUri.toString(),
233 234 235 236
          'fileContents': fileContents
        },
      );
    } catch (error) {
237
      printTrace('DevFS: Failed to write $deviceUri: $error');
238
    }
239 240
  }

241
  @override
242
  Future<dynamic> deleteFile(String fsName, Uri deviceUri) async {
243 244
    // TODO(johnmccutchan): Add file deletion to the devFS protocol.
  }
245 246
}

247 248 249 250 251 252 253
class DevFSException implements Exception {
  DevFSException(this.message, [this.error, this.stackTrace]);
  final String message;
  final dynamic error;
  final StackTrace stackTrace;
}

254
class _DevFSHttpWriter {
255
  _DevFSHttpWriter(this.fsName, VMService serviceProtocol)
256 257 258 259 260 261
      : httpAddress = serviceProtocol.httpAddress;

  final String fsName;
  final Uri httpAddress;

  static const int kMaxInFlight = 6;
262
  static const int kMaxRetries = 3;
263 264

  int _inFlight = 0;
265
  Map<Uri, DevFSContent> _outstanding;
266
  Completer<Null> _completer;
267
  HttpClient _client;
268

269
  Future<Null> write(Map<Uri, DevFSContent> entries) async {
270
    _client = new HttpClient();
271 272
    _client.maxConnectionsPerHost = kMaxInFlight;
    _completer = new Completer<Null>();
273
    _outstanding = new Map<Uri, DevFSContent>.from(entries);
274
    _scheduleWrites();
275 276 277 278
    await _completer.future;
    _client.close();
  }

279
  void _scheduleWrites() {
280
    while (_inFlight < kMaxInFlight) {
281
      if (_outstanding.isEmpty) {
282 283 284
        // Finished.
        break;
      }
285 286
      final Uri deviceUri = _outstanding.keys.first;
      final DevFSContent content = _outstanding.remove(deviceUri);
287
      _scheduleWrite(deviceUri, content);
288 289 290 291
      _inFlight++;
    }
  }

292
  Future<Null> _scheduleWrite(
293
    Uri deviceUri,
294
    DevFSContent content, [
295 296
    int retry = 0,
  ]) async {
Ryan Macnak's avatar
Ryan Macnak committed
297
    try {
298
      final HttpClientRequest request = await _client.putUrl(httpAddress);
299
      request.headers.removeAll(HttpHeaders.ACCEPT_ENCODING);
Ryan Macnak's avatar
Ryan Macnak committed
300
      request.headers.add('dev_fs_name', fsName);
301
      request.headers.add('dev_fs_uri_b64',
302
          base64.encode(utf8.encode(deviceUri.toString())));
303
      final Stream<List<int>> contents = content.contentsAsCompressedStream();
Ryan Macnak's avatar
Ryan Macnak committed
304
      await request.addStream(contents);
305
      final HttpClientResponse response = await request.close();
306
      await response.drain<Null>();
307 308 309 310 311
    } on SocketException catch (socketException, stackTrace) {
      // We have one completer and can get up to kMaxInFlight errors.
      if (!_completer.isCompleted)
        _completer.completeError(socketException, stackTrace);
      return;
312
    } catch (e) {
313
      if (retry < kMaxRetries) {
314
        printTrace('Retrying writing "$deviceUri" to DevFS due to error: $e');
315 316
        // Synchronization is handled by the _completer below.
        _scheduleWrite(deviceUri, content, retry + 1); // ignore: unawaited_futures
317 318
        return;
      } else {
319
        printError('Error writing "$deviceUri" to DevFS: $e');
320
      }
Ryan Macnak's avatar
Ryan Macnak committed
321
    }
322
    _inFlight--;
323
    if ((_outstanding.isEmpty) && (_inFlight == 0)) {
324 325
      _completer.complete(null);
    } else {
326
      _scheduleWrites();
327 328 329 330
    }
  }
}

331 332
class DevFS {
  /// Create a [DevFS] named [fsName] for the local files in [directory].
333
  DevFS(VMService serviceProtocol,
334
        this.fsName,
335 336 337
        this.rootDirectory, {
        String packagesFilePath
      })
338
    : _operations = new ServiceProtocolDevFSOperations(serviceProtocol),
339
      _httpWriter = new _DevFSHttpWriter(fsName, serviceProtocol) {
340
    _packagesFilePath =
341
        packagesFilePath ?? fs.path.join(rootDirectory.path, kPackagesFileName);
342
  }
343 344 345

  DevFS.operations(this._operations,
                   this.fsName,
346 347 348 349 350
                   this.rootDirectory, {
                   String packagesFilePath,
      })
    : _httpWriter = null {
    _packagesFilePath =
351
        packagesFilePath ?? fs.path.join(rootDirectory.path, kPackagesFileName);
352
  }
353 354

  final DevFSOperations _operations;
355
  final _DevFSHttpWriter _httpWriter;
356 357
  final String fsName;
  final Directory rootDirectory;
358
  String _packagesFilePath;
359
  final Map<Uri, DevFSContent> _entries = <Uri, DevFSContent>{};
360
  final Set<String> assetPathsToEvict = new Set<String>();
361

362
  final List<Future<Map<String, dynamic>>> _pendingOperations =
363
      <Future<Map<String, dynamic>>>[];
364

365 366 367
  Uri _baseUri;
  Uri get baseUri => _baseUri;

368 369 370 371 372 373 374 375 376 377
  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);
      return rootDirectory.uri.resolve(deviceUriSuffix);
    }
    return deviceUri;
  }

378
  Future<Uri> create() async {
379
    printTrace('DevFS: Creating new filesystem on the device ($_baseUri)');
380 381 382 383 384 385 386 387 388 389
    try {
      _baseUri = await _operations.create(fsName);
    } on rpc.RpcException catch (rpcException) {
      // 1001 is kFileSystemAlreadyExists in //dart/runtime/vm/json_stream.h
      if (rpcException.code != 1001)
        rethrow;
      printTrace('DevFS: Creating failed. Destroying and trying again');
      await destroy();
      _baseUri = await _operations.create(fsName);
    }
390 391 392 393
    printTrace('DevFS: Created new filesystem on the device ($_baseUri)');
    return _baseUri;
  }

394 395 396
  Future<Null> destroy() async {
    printTrace('DevFS: Deleting filesystem on the device ($_baseUri)');
    await _operations.destroy(fsName);
397 398 399
    printTrace('DevFS: Deleted filesystem on the device ($_baseUri)');
  }

400 401 402
  /// Updates files on the device.
  ///
  /// Returns the number of bytes synced.
403
  Future<int> update({
404
    String mainPath,
405
    String target,
406
    AssetBundle bundle,
407
    DateTime firstBuildTime,
408 409
    bool bundleFirstUpload = false,
    bool bundleDirty = false,
410
    Set<String> fileFilter,
411
    ResidentCompiler generator,
412
    String dillOutputPath,
413
    bool fullRestart = false,
414
    String projectRootPath,
415
  }) async {
416 417 418 419 420 421
    // Mark all entries as possibly deleted.
    for (DevFSContent content in _entries.values) {
      content._exists = false;
    }

    // Scan workspace, packages, and assets
422
    printTrace('DevFS: Starting sync from $rootDirectory');
423
    logger.printTrace('Scanning project files');
424
    await _scanDirectory(rootDirectory,
425 426
                         recursive: true,
                         fileFilter: fileFilter);
427
    final bool previewDart2 = generator != null;
428
    if (fs.isFileSync(_packagesFilePath)) {
429
      printTrace('Scanning package files');
430
      await _scanPackages(fileFilter, previewDart2);
431
    }
432
    if (bundle != null) {
433
      printTrace('Scanning asset files');
434
      bundle.entries.forEach((String archivePath, DevFSContent content) {
435
        _scanBundleEntry(archivePath, content);
436
      });
437
    }
438

439
    // Handle deletions.
440
    printTrace('Scanning for deleted files');
441
    final String assetBuildDirPrefix = _asUriPath(getAssetBuildDirectory());
442
    final List<Uri> toRemove = <Uri>[];
443
    _entries.forEach((Uri deviceUri, DevFSContent content) {
444
      if (!content._exists) {
445
        final Future<Map<String, dynamic>> operation =
446 447
            _operations.deleteFile(fsName, deviceUri)
            .then((dynamic v) => v?.cast<String,dynamic>());
448 449
        if (operation != null)
          _pendingOperations.add(operation);
450 451
        toRemove.add(deviceUri);
        if (deviceUri.path.startsWith(assetBuildDirPrefix)) {
452
          final String archivePath = deviceUri.path.substring(assetBuildDirPrefix.length);
453 454
          assetPathsToEvict.add(archivePath);
        }
455
      }
456 457 458 459
    });
    if (toRemove.isNotEmpty) {
      printTrace('Removing deleted files');
      toRemove.forEach(_entries.remove);
460 461 462
      await Future.wait(_pendingOperations);
      _pendingOperations.clear();
    }
463

464 465
    // Update modified files
    int numBytes = 0;
466
    final Map<Uri, DevFSContent> dirtyEntries = <Uri, DevFSContent>{};
467
    _entries.forEach((Uri deviceUri, DevFSContent content) {
468
      String archivePath;
469 470
      if (deviceUri.path.startsWith(assetBuildDirPrefix))
        archivePath = deviceUri.path.substring(assetBuildDirPrefix.length);
471 472 473 474
      // When doing full restart in preview-dart-2 mode, copy content so
      // that isModified does not reset last check timestamp because we
      // want to report all modified files to incremental compiler next time
      // user does hot reload.
475
      if (content.isModified || ((bundleDirty || bundleFirstUpload) && archivePath != null)) {
476
        dirtyEntries[deviceUri] = content;
477
        numBytes += content.size;
478
        if (archivePath != null && (!bundleFirstUpload || content.isModifiedAfter(firstBuildTime)))
479 480 481
          assetPathsToEvict.add(archivePath);
      }
    });
482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497
    if (previewDart2) {
      // We run generator even if [dirtyEntries] was empty because we want
      // to keep logic of accepting/rejecting generator's output simple:
      // we must accept/reject generator's output after every [update] call.
      // Incremental run with no changes is supposed to be fast (considering
      // that it is initiated by user key press).
      final List<String> invalidatedFiles = <String>[];
      final Set<Uri> filesUris = new Set<Uri>();
      for (Uri uri in dirtyEntries.keys.toList()) {
        if (!uri.path.startsWith(assetBuildDirPrefix)) {
          final DevFSContent content = dirtyEntries[uri];
          if (content is DevFSFileContent) {
            filesUris.add(uri);
            invalidatedFiles.add(content.file.uri.toString());
            numBytes -= content.size;
          }
498 499
        }
      }
500 501 502 503 504 505 506 507 508 509 510
      // No need to send source files because all compilation is done on the
      // host and result of compilation is single kernel file.
      filesUris.forEach(dirtyEntries.remove);
      printTrace('Compiling dart to kernel with ${invalidatedFiles.length} updated files');
      if (fullRestart) {
        generator.reset();
      }
      final CompilerOutput compilerOutput =
          await generator.recompile(mainPath, invalidatedFiles,
              outputPath:  dillOutputPath ?? fs.path.join(getBuildDirectory(), 'app.dill'),
              packagesFilePath : _packagesFilePath);
511
      final String compiledBinary = compilerOutput?.outputFilename;
512 513 514 515
      if (compiledBinary != null && compiledBinary.isNotEmpty) {
        final String entryUri = projectRootPath != null ?
            fs.path.relative(mainPath, from: projectRootPath):
            mainPath;
516 517 518 519 520 521
        final Uri kernelUri = fs.path.toUri(entryUri + '.dill');
        if (!dirtyEntries.containsKey(kernelUri)) {
          final DevFSFileContent content = new DevFSFileContent(fs.file(compiledBinary));
          dirtyEntries[kernelUri] = content;
          numBytes += content.size;
        }
522
      }
523
    }
524
    if (dirtyEntries.isNotEmpty) {
525
      printTrace('Updating files');
526
      if (_httpWriter != null) {
527
        try {
528
          await _httpWriter.write(dirtyEntries);
529
        } on SocketException catch (socketException, stackTrace) {
530
          printTrace('DevFS sync failed. Lost connection to device: $socketException');
531 532
          throw new DevFSException('Lost connection to device.', socketException, stackTrace);
        } catch (exception, stackTrace) {
533
          printError('Could not update files on device: $exception');
534
          throw new DevFSException('Sync failed', exception, stackTrace);
535
        }
536 537
      } else {
        // Make service protocol requests for each.
538
        dirtyEntries.forEach((Uri deviceUri, DevFSContent content) {
539
          final Future<Map<String, dynamic>> operation =
540 541
              _operations.writeFile(fsName, deviceUri, content)
                  .then((dynamic v) => v?.cast<String, dynamic>());
542 543
          if (operation != null)
            _pendingOperations.add(operation);
544
        });
545 546 547
        await Future.wait(_pendingOperations, eagerError: true);
        _pendingOperations.clear();
      }
548 549
    }

550
    printTrace('DevFS: Sync finished');
551
    return numBytes;
552 553
  }

554
  void _scanFile(Uri deviceUri, FileSystemEntity file) {
555
    final DevFSContent content = _entries.putIfAbsent(deviceUri, () => new DevFSFileContent(file));
556
    content._exists = true;
557 558
  }

559
  void _scanBundleEntry(String archivePath, DevFSContent content) {
560 561
    // We write the assets into the AssetBundle working dir so that they
    // are in the same location in DevFS and the iOS simulator.
562
    final Uri deviceUri = fs.path.toUri(fs.path.join(getAssetBuildDirectory(), archivePath));
563

564
    _entries[deviceUri] = content;
565
    content._exists = true;
566 567
  }

568
  bool _shouldIgnore(Uri deviceUri) {
569
    final List<String> ignoredUriPrefixes = <String>['android/',
570 571 572 573 574
                                               _asUriPath(getBuildDirectory()),
                                               'ios/',
                                               '.pub/'];
    for (String ignoredUriPrefix in ignoredUriPrefixes) {
      if (deviceUri.path.startsWith(ignoredUriPrefix))
575 576 577 578 579
        return true;
    }
    return false;
  }

580 581 582
  bool _shouldSkip(FileSystemEntity file,
                   String relativePath,
                   Uri directoryUriOnDevice, {
583
                   bool ignoreDotFiles = true,
584 585 586 587 588 589
                   }) {
    if (file is Directory) {
      // Skip non-files.
      return true;
    }
    assert((file is Link) || (file is File));
590 591 592 593 594
    final String basename = fs.path.basename(file.path);
    if (ignoreDotFiles && basename.startsWith('.')) {
      // Skip dot files, but not the '.packages' file (even though in dart1
      // mode devfs['.packages'] will be overwritten with synthesized string content).
      return basename != '.packages';
595 596 597 598 599 600
    }
    return false;
  }

  Uri _directoryUriOnDevice(Uri directoryUriOnDevice,
                            Directory directory) {
601
    if (directoryUriOnDevice == null) {
602
      final String relativeRootPath = fs.path.relative(directory.path, from: rootDirectory.path);
603 604 605 606 607
      if (relativeRootPath == '.') {
        directoryUriOnDevice = new Uri();
      } else {
        directoryUriOnDevice = fs.path.toUri(relativeRootPath);
      }
608
    }
609 610 611 612 613 614 615 616
    return directoryUriOnDevice;
  }

  /// Scan all files from the [fileFilter] that are contained in [directory] and
  /// pass various filters (e.g. ignoreDotFiles).
  Future<bool> _scanFilteredDirectory(Set<String> fileFilter,
                                      Directory directory,
                                      {Uri directoryUriOnDevice,
617
                                       bool ignoreDotFiles = true}) async {
618 619 620
    directoryUriOnDevice =
        _directoryUriOnDevice(directoryUriOnDevice, directory);
    try {
621
      final String absoluteDirectoryPath = canonicalizePath(directory.path);
622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647
      // For each file in the file filter.
      for (String filePath in fileFilter) {
        if (!filePath.startsWith(absoluteDirectoryPath)) {
          // File is not in this directory. Skip.
          continue;
        }
        final String relativePath =
          fs.path.relative(filePath, from: directory.path);
        final FileSystemEntity file = fs.file(filePath);
        if (_shouldSkip(file, relativePath, directoryUriOnDevice, ignoreDotFiles: ignoreDotFiles)) {
          continue;
        }
        final Uri deviceUri = directoryUriOnDevice.resolveUri(fs.path.toUri(relativePath));
        if (!_shouldIgnore(deviceUri))
          _scanFile(deviceUri, file);
      }
    } on FileSystemException catch (e) {
      _printScanDirectoryError(directory.path, e);
      return false;
    }
    return true;
  }

  /// Scan all files in [directory] that pass various filters (e.g. ignoreDotFiles).
  Future<bool> _scanDirectory(Directory directory,
                              {Uri directoryUriOnDevice,
648 649
                               bool recursive = false,
                               bool ignoreDotFiles = true,
650 651 652 653 654 655 656 657 658 659
                               Set<String> fileFilter}) async {
    directoryUriOnDevice = _directoryUriOnDevice(directoryUriOnDevice, directory);
    if ((fileFilter != null) && fileFilter.isNotEmpty) {
      // When the fileFilter isn't empty, we can skip crawling the directory
      // tree and instead use the fileFilter as the source of potential files.
      return _scanFilteredDirectory(fileFilter,
                                    directory,
                                    directoryUriOnDevice: directoryUriOnDevice,
                                    ignoreDotFiles: ignoreDotFiles);
    }
660
    try {
661
      final Stream<FileSystemEntity> files =
662 663
          directory.list(recursive: recursive, followLinks: false);
      await for (FileSystemEntity file in files) {
664 665
        if (!devFSConfig.noDirectorySymlinks && (file is Link)) {
          // Check if this is a symlink to a directory and skip it.
666 667 668
          try {
            final FileSystemEntityType linkType =
                fs.statSync(file.resolveSymbolicLinksSync()).type;
669
            if (linkType == FileSystemEntityType.DIRECTORY) // ignore: deprecated_member_use
670 671 672
              continue;
          } on FileSystemException catch (e) {
            _printScanDirectoryError(file.path, e);
673 674 675
            continue;
          }
        }
676
        final String relativePath =
677
          fs.path.relative(file.path, from: directory.path);
678
        if (_shouldSkip(file, relativePath, directoryUriOnDevice, ignoreDotFiles: ignoreDotFiles)) {
679 680
          continue;
        }
681
        final Uri deviceUri = directoryUriOnDevice.resolveUri(fs.path.toUri(relativePath));
682 683
        if (!_shouldIgnore(deviceUri))
          _scanFile(deviceUri, file);
684
      }
685 686
    } on FileSystemException catch (e) {
      _printScanDirectoryError(directory.path, e);
687 688 689 690
      return false;
    }
    return true;
  }
691

692 693 694 695 696 697 698 699
  void _printScanDirectoryError(String path, Exception e) {
    printError(
        'Error while scanning $path.\n'
        'Hot Reload might not work until the following error is resolved:\n'
        '$e\n'
    );
  }

700
  Future<Null> _scanPackages(Set<String> fileFilter, bool previewDart2) async {
701
    StringBuffer sb;
702
    final PackageMap packageMap = new PackageMap(_packagesFilePath);
703 704

    for (String packageName in packageMap.map.keys) {
705
      final Uri packageUri = packageMap.map[packageName];
706
      final String packagePath = fs.path.fromUri(packageUri);
707
      final Directory packageDirectory = fs.directory(packageUri);
708
      Uri directoryUriOnDevice = fs.path.toUri(fs.path.join('packages', packageName) + fs.path.separator);
709 710 711 712 713 714
      bool packageExists = packageDirectory.existsSync();

      if (!packageExists) {
        // If the package directory doesn't exist at all, we ignore it.
        continue;
      }
715 716 717

      if (fs.path.isWithin(rootDirectory.path, packagePath)) {
        // We already scanned everything under the root directory.
718 719 720
        directoryUriOnDevice = fs.path.toUri(
            fs.path.relative(packagePath, from: rootDirectory.path) + fs.path.separator
        );
721 722 723
      } else {
        packageExists =
            await _scanDirectory(packageDirectory,
724
                                 directoryUriOnDevice: directoryUriOnDevice,
725 726 727
                                 recursive: true,
                                 fileFilter: fileFilter);
      }
728 729
      if (packageExists) {
        sb ??= new StringBuffer();
730
        sb.writeln('$packageName:$directoryUriOnDevice');
731 732
      }
    }
733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748
    if (previewDart2) {
      // When in previewDart2 mode we don't update .packages-file entry
      // so actual file will get invalidated in frontend.
      // We don't need to synthesize device-correct .packages file because
      // it is not going to be used on the device anyway - compilation
      // is done on the host.
      return;
    }
    if (sb != null) {
      final DevFSContent content = _entries[fs.path.toUri('.packages')];
      if (content is DevFSStringContent && content.string == sb.toString()) {
        content._exists = true;
        return;
      }
      _entries[fs.path.toUri('.packages')] = new DevFSStringContent(sb.toString());
    }
749
  }
750
}
751 752
/// Converts a platform-specific file path to a platform-independent Uri path.
String _asUriPath(String filePath) => fs.path.toUri(filePath).path + '/';