devfs.dart 17.9 KB
Newer Older
1 2 3 4 5 6 7 8 9 10
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
import 'dart:convert' show BASE64, UTF8;
import 'dart:io';

import 'package:path/path.dart' as path;

11
import 'base/logger.dart';
12
import 'build_info.dart';
13
import 'dart/package_map.dart';
14
import 'asset.dart';
15
import 'globals.dart';
16
import 'vmservice.dart';
17

18 19
typedef void DevFSProgressReporter(int progress, int max);

20 21
// A file that has been added to a DevFS.
class DevFSEntry {
22 23 24 25 26 27
  DevFSEntry(this.devicePath, this.file)
      : bundleEntry = null;

  DevFSEntry.bundle(this.devicePath, AssetBundleEntry bundleEntry)
      : bundleEntry = bundleEntry,
        file = bundleEntry.file;
28 29

  final String devicePath;
30
  final AssetBundleEntry bundleEntry;
31
  String get assetPath => bundleEntry.archivePath;
32

33
  final FileSystemEntity file;
34
  FileStat _fileStat;
35 36
  // When we scanned for files, did this still exist?
  bool _exists = false;
37 38
  DateTime get lastModified => _fileStat?.modified;
  bool get stillExists {
39 40
    if (_isSourceEntry)
      return true;
41 42 43 44
    _stat();
    return _fileStat.type != FileSystemEntityType.NOT_FOUND;
  }
  bool get isModified {
45 46 47
    if (_isSourceEntry)
      return true;

48 49 50 51 52 53 54 55 56
    if (_fileStat == null) {
      _stat();
      return true;
    }
    FileStat _oldFileStat = _fileStat;
    _stat();
    return _fileStat.modified.isAfter(_oldFileStat.modified);
  }

57 58 59 60 61 62 63 64 65 66 67
  int get size {
    if (_isSourceEntry) {
      return bundleEntry.contentsLength;
    } else {
      if (_fileStat == null) {
        _stat();
      }
      return _fileStat.size;
    }
  }

68
  void _stat() {
69 70
    if (_isSourceEntry)
      return;
71
    _fileStat = file.statSync();
72 73 74 75 76
    if (_fileStat.type == FileSystemEntityType.LINK) {
      // Stat the link target.
      String resolved = file.resolveSymbolicLinksSync();
      _fileStat = FileStat.statSync(resolved);
    }
77
  }
78 79 80

  bool get _isSourceEntry => file == null;

81 82
  bool get _isAssetEntry => bundleEntry != null;

83 84 85 86 87 88 89 90
  File _getFile() {
    if (file is Link) {
      // The link target.
      return new File(file.resolveSymbolicLinksSync());
    }
    return file;
  }

91 92 93
  Future<List<int>> contentsAsBytes() async {
    if (_isSourceEntry)
      return bundleEntry.contentsAsBytes();
94
    final File file = _getFile();
95 96
    return file.readAsBytes();
  }
97 98 99 100 101 102

  Stream<List<int>> contentsAsStream() {
    if (_isSourceEntry) {
      return new Stream<List<int>>.fromIterable(
          <List<int>>[bundleEntry.contentsAsBytes()]);
    }
103
    final File file = _getFile();
104 105 106 107 108 109
    return file.openRead();
  }

  Stream<List<int>> contentsAsCompressedStream() {
    return contentsAsStream().transform(GZIP.encoder);
  }
110 111 112 113 114 115 116 117
}


/// Abstract DevFS operations interface.
abstract class DevFSOperations {
  Future<Uri> create(String fsName);
  Future<dynamic> destroy(String fsName);
  Future<dynamic> writeFile(String fsName, DevFSEntry entry);
118
  Future<dynamic> deleteFile(String fsName, DevFSEntry entry);
119 120 121 122 123 124
  Future<dynamic> writeSource(String fsName,
                              String devicePath,
                              String contents);
}

/// An implementation of [DevFSOperations] that speaks to the
125
/// vm service.
126
class ServiceProtocolDevFSOperations implements DevFSOperations {
127
  final VMService vmService;
128

129
  ServiceProtocolDevFSOperations(this.vmService);
130 131 132

  @override
  Future<Uri> create(String fsName) async {
133
    Map<String, dynamic> response = await vmService.vm.createDevFS(fsName);
134 135 136 137 138
    return Uri.parse(response['uri']);
  }

  @override
  Future<dynamic> destroy(String fsName) async {
139 140
    await vmService.vm.invokeRpcRaw('_deleteDevFS',
                                    <String, dynamic> { 'fsName': fsName });
141 142 143 144 145 146
  }

  @override
  Future<dynamic> writeFile(String fsName, DevFSEntry entry) async {
    List<int> bytes;
    try {
147
      bytes = await entry.contentsAsBytes();
148 149 150 151
    } catch (e) {
      return e;
    }
    String fileContents = BASE64.encode(bytes);
152
    try {
153 154 155 156 157 158
      return await vmService.vm.invokeRpcRaw('_writeDevFSFile',
                                             <String, dynamic> {
                                                'fsName': fsName,
                                                'path': entry.devicePath,
                                                'fileContents': fileContents
                                             });
159
    } catch (e) {
John McCutchan's avatar
John McCutchan committed
160
      printTrace('DevFS: Failed to write ${entry.devicePath}: $e');
161
    }
162 163
  }

164 165 166 167 168
  @override
  Future<dynamic> deleteFile(String fsName, DevFSEntry entry) async {
    // TODO(johnmccutchan): Add file deletion to the devFS protocol.
  }

169 170 171 172 173
  @override
  Future<dynamic> writeSource(String fsName,
                              String devicePath,
                              String contents) async {
    String fileContents = BASE64.encode(UTF8.encode(contents));
174 175 176 177 178 179
    return await vmService.vm.invokeRpcRaw('_writeDevFSFile',
                                           <String, dynamic> {
                                              'fsName': fsName,
                                              'path': devicePath,
                                              'fileContents': fileContents
                                           });
180 181 182
  }
}

183
class _DevFSHttpWriter {
184
  _DevFSHttpWriter(this.fsName, VMService serviceProtocol)
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246
      : httpAddress = serviceProtocol.httpAddress;

  final String fsName;
  final Uri httpAddress;

  static const int kMaxInFlight = 6;

  int _inFlight = 0;
  List<DevFSEntry> _outstanding;
  Completer<Null> _completer;
  HttpClient _client;
  int _done;
  int _max;

  Future<Null> write(Set<DevFSEntry> entries,
                     {DevFSProgressReporter progressReporter}) async {
    _client = new HttpClient();
    _client.maxConnectionsPerHost = kMaxInFlight;
    _completer = new Completer<Null>();
    _outstanding = entries.toList();
    _done = 0;
    _max = _outstanding.length;
    _scheduleWrites(progressReporter);
    await _completer.future;
    _client.close();
  }

  void _scheduleWrites(DevFSProgressReporter progressReporter) {
    while (_inFlight < kMaxInFlight) {
       if (_outstanding.length == 0) {
        // Finished.
        break;
      }
      DevFSEntry entry = _outstanding.removeLast();
      _scheduleWrite(entry, progressReporter);
      _inFlight++;
    }
  }

  Future<Null> _scheduleWrite(DevFSEntry entry,
                              DevFSProgressReporter progressReporter) async {
    HttpClientRequest request = await _client.putUrl(httpAddress);
    request.headers.removeAll(HttpHeaders.ACCEPT_ENCODING);
    request.headers.add('dev_fs_name', fsName);
    request.headers.add('dev_fs_path', entry.devicePath);
    Stream<List<int>> contents = entry.contentsAsCompressedStream();
    await request.addStream(contents);
    HttpClientResponse response = await request.close();
    await response.drain();
    if (progressReporter != null) {
      _done++;
      progressReporter(_done, _max);
    }
    _inFlight--;
    if ((_outstanding.length == 0) && (_inFlight == 0)) {
      _completer.complete(null);
    } else {
      _scheduleWrites(progressReporter);
    }
  }
}

247 248
class DevFS {
  /// Create a [DevFS] named [fsName] for the local files in [directory].
249
  DevFS(VMService serviceProtocol,
250
        String fsName,
251
        this.rootDirectory)
252 253 254
    : _operations = new ServiceProtocolDevFSOperations(serviceProtocol),
      _httpWriter = new _DevFSHttpWriter(fsName, serviceProtocol),
      fsName = fsName;
255 256 257

  DevFS.operations(this._operations,
                   this.fsName,
258 259
                   this.rootDirectory)
    : _httpWriter = null;
260 261

  final DevFSOperations _operations;
262
  final _DevFSHttpWriter _httpWriter;
263 264 265
  final String fsName;
  final Directory rootDirectory;
  final Map<String, DevFSEntry> _entries = <String, DevFSEntry>{};
266 267
  final Set<DevFSEntry> _dirtyEntries = new Set<DevFSEntry>();
  final Set<DevFSEntry> _deletedEntries = new Set<DevFSEntry>();
268
  final Set<DevFSEntry> dirtyAssetEntries = new Set<DevFSEntry>();
269

270 271
  final List<Future<Map<String, dynamic>>> _pendingOperations =
      new List<Future<Map<String, dynamic>>>();
272

273 274
  int _bytes = 0;
  int get bytes => _bytes;
275 276 277 278 279 280 281 282 283
  Uri _baseUri;
  Uri get baseUri => _baseUri;

  Future<Uri> create() async {
    _baseUri = await _operations.create(fsName);
    printTrace('DevFS: Created new filesystem on the device ($_baseUri)');
    return _baseUri;
  }

284
  Future<dynamic> destroy() {
285
    printTrace('DevFS: Deleted filesystem on the device ($_baseUri)');
286
    return _operations.destroy(fsName);
287 288
  }

289 290
  void _reset() {
    // Reset the dirty byte count.
291
    _bytes = 0;
292
    // Mark all entries as possibly deleted.
293
    _entries.forEach((String path, DevFSEntry entry) {
294
      entry._exists = false;
295
    });
296 297 298 299
    // Clear the dirt entries list.
    _dirtyEntries.clear();
    // Clear the deleted entries list.
    _deletedEntries.clear();
300 301
    // Clear the dirty asset entries.
    dirtyAssetEntries.clear();
302 303 304 305
  }

  Future<dynamic> update({ DevFSProgressReporter progressReporter,
                           AssetBundle bundle,
306 307
                           bool bundleDirty: false,
                           Set<String> fileFilter}) async {
308
    _reset();
309
    printTrace('DevFS: Starting sync from $rootDirectory');
310 311
    Status status;
    status = logger.startProgress('Scanning project files...');
312
    Directory directory = rootDirectory;
313 314 315
    await _scanDirectory(directory,
                         recursive: true,
                         fileFilter: fileFilter);
316 317 318
    status.stop(showElapsedTime: true);

    status = logger.startProgress('Scanning package files...');
319 320 321 322 323 324 325
    String packagesFilePath = path.join(rootDirectory.path, kPackagesFileName);
    StringBuffer sb;
    if (FileSystemEntity.isFileSync(packagesFilePath)) {
      PackageMap packageMap = new PackageMap(kPackagesFileName);

      for (String packageName in packageMap.map.keys) {
        Uri uri = packageMap.map[packageName];
326 327 328 329 330 331 332 333 334 335
        // This project's own package.
        final bool isProjectPackage = uri.toString() == 'lib/';
        final String directoryName =
            isProjectPackage ? 'lib' : 'packages/$packageName';
        // If this is the project's package, we need to pass both
        // package:<package_name> and lib/ as paths to be checked against
        // the filter because we must support both package: imports and relative
        // path imports within the project's own code.
        final String packagesDirectoryName =
            isProjectPackage ? 'packages/$packageName' : null;
336
        Directory directory = new Directory.fromUri(uri);
337 338
        bool packageExists =
            await _scanDirectory(directory,
339
                                 directoryName: directoryName,
340
                                 recursive: true,
341
                                 packagesDirectoryName: packagesDirectoryName,
342
                                 fileFilter: fileFilter);
343
        if (packageExists) {
344
          sb ??= new StringBuffer();
345
          sb.writeln('$packageName:$directoryName');
346 347 348
        }
      }
    }
349
    status.stop(showElapsedTime: true);
350
    if (bundle != null) {
351
      status = logger.startProgress('Scanning asset files...');
352 353
      // Synchronize asset bundle.
      for (AssetBundleEntry entry in bundle.entries) {
354 355 356 357
        // We write the assets into the AssetBundle working dir so that they
        // are in the same location in DevFS and the iOS simulator.
        final String devicePath =
            path.join(getAssetBuildDirectory(), entry.archivePath);
358
        _scanBundleEntry(devicePath, entry, bundleDirty);
359
      }
360
      status.stop(showElapsedTime: true);
361 362
    }
    // Handle deletions.
363
    status = logger.startProgress('Scanning for deleted files...');
364 365
    final List<String> toRemove = new List<String>();
    _entries.forEach((String path, DevFSEntry entry) {
366 367
      if (!entry._exists) {
        _deletedEntries.add(entry);
368 369 370 371 372 373
        toRemove.add(path);
      }
    });
    for (int i = 0; i < toRemove.length; i++) {
      _entries.remove(toRemove[i]);
    }
374 375 376 377 378
    status.stop(showElapsedTime: true);

    if (_deletedEntries.length > 0) {
      status = logger.startProgress('Removing deleted files...');
      for (DevFSEntry entry in _deletedEntries) {
379 380
        Future<Map<String, dynamic>> operation =
            _operations.deleteFile(fsName, entry);
381 382 383 384 385 386 387 388 389 390
        if (operation != null)
          _pendingOperations.add(operation);
      }
      await Future.wait(_pendingOperations);
      _pendingOperations.clear();
      _deletedEntries.clear();
      status.stop(showElapsedTime: true);
    } else {
      printStatus("No files to remove.");
    }
391

392 393 394
    if (_dirtyEntries.length > 0) {
      status = logger.startProgress('Updating files...');
      if (_httpWriter != null) {
395 396 397 398 399 400
        try {
          await _httpWriter.write(_dirtyEntries,
                                  progressReporter: progressReporter);
        } catch (e) {
          printError("Could not update files on device: $e");
        }
401 402 403
      } else {
        // Make service protocol requests for each.
        for (DevFSEntry entry in _dirtyEntries) {
404 405
          Future<Map<String, dynamic>> operation =
              _operations.writeFile(fsName, entry);
406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423
          if (operation != null)
            _pendingOperations.add(operation);
        }
        if (progressReporter != null) {
          final int max = _pendingOperations.length;
          int complete = 0;
          _pendingOperations.forEach((Future<dynamic> f) => f.then((dynamic v) {
            complete += 1;
            progressReporter(complete, max);
          }));
        }
        await Future.wait(_pendingOperations, eagerError: true);
        _pendingOperations.clear();
      }
      _dirtyEntries.clear();
      status.stop(showElapsedTime: true);
    } else {
      printStatus("No files to update.");
424 425 426
    }

    if (sb != null)
427
      await _operations.writeSource(fsName, '.packages', sb.toString());
428

429 430 431 432 433 434
    printTrace('DevFS: Sync finished');
    // NB: You must call flush after a printTrace if you want to be printed
    // immediately.
    logger.flush();
  }

435
  void _scanFile(String devicePath, FileSystemEntity file) {
436 437 438 439 440 441
    DevFSEntry entry = _entries[devicePath];
    if (entry == null) {
      // New file.
      entry = new DevFSEntry(devicePath, file);
      _entries[devicePath] = entry;
    }
442
    entry._exists = true;
443 444
    bool needsWrite = entry.isModified;
    if (needsWrite) {
445 446
      if (_dirtyEntries.add(entry))
        _bytes += entry.size;
447 448 449
    }
  }

450 451 452
  void _scanBundleEntry(String devicePath,
                        AssetBundleEntry assetBundleEntry,
                        bool bundleDirty) {
453 454 455 456 457 458
    DevFSEntry entry = _entries[devicePath];
    if (entry == null) {
      // New file.
      entry = new DevFSEntry.bundle(devicePath, assetBundleEntry);
      _entries[devicePath] = entry;
    }
459 460 461 462 463 464
    entry._exists = true;
    if (!bundleDirty && assetBundleEntry.isStringEntry) {
      // String bundle entries are synthetic files that only change if the
      // bundle itself changes. Skip them if the bundle is not dirty.
      return;
    }
465 466
    bool needsWrite = entry.isModified;
    if (needsWrite) {
467
      if (_dirtyEntries.add(entry)) {
468
        _bytes += entry.size;
469 470 471
        if (entry._isAssetEntry)
          dirtyAssetEntries.add(entry);
      }
472 473 474 475
    }
  }

  bool _shouldIgnore(String devicePath) {
476
    List<String> ignoredPrefixes = <String>['android/',
477
                                            getBuildDirectory(),
478
                                            'ios/',
479
                                            '.pub/'];
480
    for (String ignoredPrefix in ignoredPrefixes) {
481
      if (devicePath.startsWith(ignoredPrefix))
482 483 484 485 486
        return true;
    }
    return false;
  }

487
  Future<bool> _scanDirectory(Directory directory,
488 489
                              {String directoryName,
                               bool recursive: false,
490
                               bool ignoreDotFiles: true,
491
                               String packagesDirectoryName,
492
                               Set<String> fileFilter}) async {
493 494 495 496 497 498 499
    String prefix = directoryName;
    if (prefix == null) {
      prefix = path.relative(directory.path, from: rootDirectory.path);
      if (prefix == '.')
        prefix = '';
    }
    try {
500 501 502
      Stream<FileSystemEntity> files =
          directory.list(recursive: recursive, followLinks: false);
      await for (FileSystemEntity file in files) {
503 504 505 506 507 508 509 510 511 512
        if (file is Link) {
          final String linkPath = file.resolveSymbolicLinksSync();
          final FileSystemEntityType linkType =
              FileStat.statSync(linkPath).type;
          if (linkType == FileSystemEntityType.DIRECTORY) {
            // Skip links to directories.
            continue;
          }
        }
        if (file is Directory) {
513 514 515
          // Skip non-files.
          continue;
        }
516
        assert((file is Link) || (file is File));
517 518 519 520
        if (ignoreDotFiles && path.basename(file.path).startsWith('.')) {
          // Skip dot files.
          continue;
        }
521 522 523 524
        final String relativePath =
            path.relative(file.path, from: directory.path);
        final String devicePath = path.join(prefix, relativePath);
        bool filtered = false;
525 526
        if ((fileFilter != null) &&
            !fileFilter.contains(devicePath)) {
527 528 529 530 531 532 533 534 535 536 537 538 539 540
          if (packagesDirectoryName != null) {
            // Double check the filter for packages/packagename/
            final String packagesDevicePath =
                path.join(packagesDirectoryName, relativePath);
            if (!fileFilter.contains(packagesDevicePath)) {
              // File was not in the filter set.
              filtered = true;
            }
          } else {
            // File was not in the filter set.
            filtered = true;
          }
        }
        if (filtered) {
541 542 543 544 545 546 547
          // Skip files that are not included in the filter.
          continue;
        }
        if (ignoreDotFiles && devicePath.startsWith('.')) {
          // Skip directories that start with a dot.
          continue;
        }
548
        if (!_shouldIgnore(devicePath))
549
          _scanFile(devicePath, file);
550 551 552 553 554 555 556 557
      }
    } catch (e) {
      // Ignore directory and error.
      return false;
    }
    return true;
  }
}