devfs.dart 25.3 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
  /// Update files on the device and return the number of bytes sync'd
401
  Future<int> update({
402 403
    String mainPath,
    String target,
404
    AssetBundle bundle,
405 406
    DateTime firstBuildTime,
    bool bundleFirstUpload: false,
407 408
    bool bundleDirty: false,
    Set<String> fileFilter,
409
    ResidentCompiler generator,
410
    String dillOutputPath,
411
    bool fullRestart: false,
412
    String projectRootPath,
413
  }) async {
414 415 416 417 418 419
    // Mark all entries as possibly deleted.
    for (DevFSContent content in _entries.values) {
      content._exists = false;
    }

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

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

461 462
    // Update modified files
    int numBytes = 0;
463
    final Map<Uri, DevFSContent> dirtyEntries = <Uri, DevFSContent>{};
464
    _entries.forEach((Uri deviceUri, DevFSContent content) {
465
      String archivePath;
466 467
      if (deviceUri.path.startsWith(assetBuildDirPrefix))
        archivePath = deviceUri.path.substring(assetBuildDirPrefix.length);
468 469 470 471
      // 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.
472
      if (content.isModified || ((bundleDirty || bundleFirstUpload) && archivePath != null)) {
473
        dirtyEntries[deviceUri] = content;
474
        numBytes += content.size;
475
        if (archivePath != null && (!bundleFirstUpload || content.isModifiedAfter(firstBuildTime)))
476 477 478
          assetPathsToEvict.add(archivePath);
      }
    });
479
    if (previewDart2) {
480 481 482 483 484 485
      // 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>[];
486 487 488 489 490 491
      final Set<Uri> filesUris = new Set<Uri>();
      for (Uri uri in dirtyEntries.keys) {
        if (!uri.path.startsWith(assetBuildDirPrefix)) {
          final DevFSContent content = dirtyEntries[uri];
          if (content is DevFSFileContent) {
            filesUris.add(uri);
492
            invalidatedFiles.add(content.file.uri.toString());
493 494 495 496 497 498
            numBytes -= content.size;
          }
        }
      }
      // No need to send source files because all compilation is done on the
      // host and result of compilation is single kernel file.
499
      filesUris.forEach(dirtyEntries.remove);
500
      printTrace('Compiling dart to kernel with ${invalidatedFiles.length} updated files');
501 502 503
      if (fullRestart) {
        generator.reset();
      }
504
      final CompilerOutput compilerOutput =
505
          await generator.recompile(mainPath, invalidatedFiles,
506 507
              outputPath:  dillOutputPath ?? fs.path.join(getBuildDirectory(), 'app.dill'),
              packagesFilePath : _packagesFilePath);
508
      final String compiledBinary = compilerOutput?.outputFilename;
509 510 511 512
      if (compiledBinary != null && compiledBinary.isNotEmpty) {
        final String entryUri = projectRootPath != null ?
            fs.path.relative(mainPath, from: projectRootPath):
            mainPath;
513
        dirtyEntries.putIfAbsent(
514
          fs.path.toUri(entryUri + '.dill'),
515 516
          () => new DevFSFileContent(fs.file(compiledBinary))
        );
517 518
      }

519
    }
520
    if (dirtyEntries.isNotEmpty) {
521
      printTrace('Updating files');
522
      if (_httpWriter != null) {
523
        try {
524
          await _httpWriter.write(dirtyEntries);
525
        } on SocketException catch (socketException, stackTrace) {
526
          printTrace('DevFS sync failed. Lost connection to device: $socketException');
527 528
          throw new DevFSException('Lost connection to device.', socketException, stackTrace);
        } catch (exception, stackTrace) {
529
          printError('Could not update files on device: $exception');
530
          throw new DevFSException('Sync failed', exception, stackTrace);
531
        }
532 533
      } else {
        // Make service protocol requests for each.
534
        dirtyEntries.forEach((Uri deviceUri, DevFSContent content) {
535
          final Future<Map<String, dynamic>> operation =
536
              _operations.writeFile(fsName, deviceUri, content);
537 538
          if (operation != null)
            _pendingOperations.add(operation);
539
        });
540 541 542
        await Future.wait(_pendingOperations, eagerError: true);
        _pendingOperations.clear();
      }
543 544
    }

545
    printTrace('DevFS: Sync finished');
546
    return numBytes;
547 548
  }

549
  void _scanFile(Uri deviceUri, FileSystemEntity file) {
550
    final DevFSContent content = _entries.putIfAbsent(deviceUri, () => new DevFSFileContent(file));
551
    content._exists = true;
552 553
  }

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

559
    _entries[deviceUri] = content;
560
    content._exists = true;
561 562
  }

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

575 576 577 578 579 580 581 582 583 584
  bool _shouldSkip(FileSystemEntity file,
                   String relativePath,
                   Uri directoryUriOnDevice, {
                   bool ignoreDotFiles: true,
                   }) {
    if (file is Directory) {
      // Skip non-files.
      return true;
    }
    assert((file is Link) || (file is File));
585 586 587 588 589
    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';
590 591 592 593 594 595
    }
    return false;
  }

  Uri _directoryUriOnDevice(Uri directoryUriOnDevice,
                            Directory directory) {
596
    if (directoryUriOnDevice == null) {
597
      final String relativeRootPath = fs.path.relative(directory.path, from: rootDirectory.path);
598 599 600 601 602
      if (relativeRootPath == '.') {
        directoryUriOnDevice = new Uri();
      } else {
        directoryUriOnDevice = fs.path.toUri(relativeRootPath);
      }
603
    }
604 605 606 607 608 609 610 611 612 613 614 615
    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,
                                       bool ignoreDotFiles: true}) async {
    directoryUriOnDevice =
        _directoryUriOnDevice(directoryUriOnDevice, directory);
    try {
616
      final String absoluteDirectoryPath = canonicalizePath(directory.path);
617 618 619 620 621 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 648 649 650 651 652 653 654
      // 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,
                               bool recursive: false,
                               bool ignoreDotFiles: true,
                               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);
    }
655
    try {
656
      final Stream<FileSystemEntity> files =
657 658
          directory.list(recursive: recursive, followLinks: false);
      await for (FileSystemEntity file in files) {
659 660
        if (!devFSConfig.noDirectorySymlinks && (file is Link)) {
          // Check if this is a symlink to a directory and skip it.
661 662 663
          try {
            final FileSystemEntityType linkType =
                fs.statSync(file.resolveSymbolicLinksSync()).type;
664
            if (linkType == FileSystemEntityType.DIRECTORY) // ignore: deprecated_member_use
665 666 667
              continue;
          } on FileSystemException catch (e) {
            _printScanDirectoryError(file.path, e);
668 669 670
            continue;
          }
        }
671
        final String relativePath =
672
          fs.path.relative(file.path, from: directory.path);
673
        if (_shouldSkip(file, relativePath, directoryUriOnDevice, ignoreDotFiles: ignoreDotFiles)) {
674 675
          continue;
        }
676
        final Uri deviceUri = directoryUriOnDevice.resolveUri(fs.path.toUri(relativePath));
677 678
        if (!_shouldIgnore(deviceUri))
          _scanFile(deviceUri, file);
679
      }
680 681
    } on FileSystemException catch (e) {
      _printScanDirectoryError(directory.path, e);
682 683 684 685
      return false;
    }
    return true;
  }
686

687 688 689 690 691 692 693 694
  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'
    );
  }

695
  Future<Null> _scanPackages(Set<String> fileFilter, bool previewDart2) async {
696
    StringBuffer sb;
697
    final PackageMap packageMap = new PackageMap(_packagesFilePath);
698 699

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

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

      if (fs.path.isWithin(rootDirectory.path, packagePath)) {
        // We already scanned everything under the root directory.
713 714 715
        directoryUriOnDevice = fs.path.toUri(
            fs.path.relative(packagePath, from: rootDirectory.path) + fs.path.separator
        );
716 717 718
      } else {
        packageExists =
            await _scanDirectory(packageDirectory,
719
                                 directoryUriOnDevice: directoryUriOnDevice,
720 721 722
                                 recursive: true,
                                 fileFilter: fileFilter);
      }
723 724
      if (packageExists) {
        sb ??= new StringBuffer();
725
        sb.writeln('$packageName:$directoryUriOnDevice');
726 727
      }
    }
728 729 730 731 732 733 734 735
    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;
    }
736
    if (sb != null) {
737
      final DevFSContent content = _entries[fs.path.toUri('.packages')];
738 739 740 741
      if (content is DevFSStringContent && content.string == sb.toString()) {
        content._exists = true;
        return;
      }
742
      _entries[fs.path.toUri('.packages')] = new DevFSStringContent(sb.toString());
743 744
    }
  }
745
}
746 747
/// Converts a platform-specific file path to a platform-independent Uri path.
String _asUriPath(String filePath) => fs.path.toUri(filePath).path + '/';