devfs.dart 18.1 KB
Newer Older
1 2 3 4 5 6 7
// 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;

8
import 'asset.dart';
9
import 'base/context.dart';
10
import 'base/file_system.dart';
11
import 'base/io.dart';
12
import 'build_info.dart';
13 14
import 'dart/package_map.dart';
import 'globals.dart';
15
import 'vmservice.dart';
16

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

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

DevFSConfig get devFSConfig => context[DevFSConfig];

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

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

36 37 38 39 40 41 42 43 44 45 46 47 48 49
  int get size;

  Future<List<int>> contentsAsBytes();

  Stream<List<int>> contentsAsStream();

  Stream<List<int>> contentsAsCompressedStream() {
    return contentsAsStream().transform(GZIP.encoder);
  }
}

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

51
  final FileSystemEntity file;
52
  FileSystemEntity _linkTarget;
53
  FileStat _fileStat;
54

55 56 57
  File _getFile() {
    if (_linkTarget != null) {
      return _linkTarget;
58
    }
59 60 61
    if (file is Link) {
      // The link target.
      return fs.file(file.resolveSymbolicLinksSync());
62
    }
63
    return file;
64 65
  }

66
  void _stat() {
67 68 69 70 71
    if (_linkTarget != null) {
      // Stat the cached symlink target.
      _fileStat = _linkTarget.statSync();
      return;
    }
72
    _fileStat = file.statSync();
73
    if (_fileStat.type == FileSystemEntityType.LINK) {
74
      // Resolve, stat, and maybe cache the symlink target.
75 76
      final String resolved = file.resolveSymbolicLinksSync();
      final FileSystemEntity linkTarget = fs.file(resolved);
77 78 79 80 81
      // Stat the link target.
      _fileStat = linkTarget.statSync();
      if (devFSConfig.cacheSymlinks) {
        _linkTarget = linkTarget;
      }
82
    }
83
  }
84

85 86
  @override
  bool get isModified {
87
    final FileStat _oldFileStat = _fileStat;
88 89
    _stat();
    return _oldFileStat == null || _fileStat.modified.isAfter(_oldFileStat.modified);
90 91
  }

92 93 94 95 96
  @override
  int get size {
    if (_fileStat == null)
      _stat();
    return _fileStat.size;
97
  }
98

99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
  @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;

  List<int> get bytes => _bytes;

116 117
  set bytes(List<int> value) {
    _bytes = value;
118
    _isModified = true;
119 120
  }

121 122 123
  /// Return `true` only once so that the content is written to the device only once.
  @override
  bool get isModified {
124
    final bool modified = _isModified;
125 126
    _isModified = false;
    return modified;
127
  }
128 129 130 131 132 133 134 135 136 137

  @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]);
138 139
}

140 141 142 143 144 145 146 147
/// String content to be copied to the device.
class DevFSStringContent extends DevFSByteContent {
  DevFSStringContent(String string) : _string = string, super(UTF8.encode(string));

  String _string;

  String get string => _string;

148 149
  set string(String value) {
    _string = value;
150 151 152 153
    super.bytes = UTF8.encode(_string);
  }

  @override
154 155
  set bytes(List<int> value) {
    string = UTF8.decode(value);
156 157
  }
}
158 159 160 161 162

/// Abstract DevFS operations interface.
abstract class DevFSOperations {
  Future<Uri> create(String fsName);
  Future<dynamic> destroy(String fsName);
163 164
  Future<dynamic> writeFile(String fsName, Uri deviceUri, DevFSContent content);
  Future<dynamic> deleteFile(String fsName, Uri deviceUri);
165 166 167
}

/// An implementation of [DevFSOperations] that speaks to the
168
/// vm service.
169
class ServiceProtocolDevFSOperations implements DevFSOperations {
170
  final VMService vmService;
171

172
  ServiceProtocolDevFSOperations(this.vmService);
173 174 175

  @override
  Future<Uri> create(String fsName) async {
176
    final Map<String, dynamic> response = await vmService.vm.createDevFS(fsName);
177 178 179 180 181
    return Uri.parse(response['uri']);
  }

  @override
  Future<dynamic> destroy(String fsName) async {
182 183 184 185
    await vmService.vm.invokeRpcRaw(
      '_deleteDevFS',
      params: <String, dynamic> { 'fsName': fsName },
    );
186 187 188
  }

  @override
189
  Future<dynamic> writeFile(String fsName, Uri deviceUri, DevFSContent content) async {
190 191
    List<int> bytes;
    try {
192
      bytes = await content.contentsAsBytes();
193 194 195
    } catch (e) {
      return e;
    }
196
    final String fileContents = BASE64.encode(bytes);
197
    try {
198 199 200 201
      return await vmService.vm.invokeRpcRaw(
        '_writeDevFSFile',
        params: <String, dynamic> {
          'fsName': fsName,
202
          'uri': deviceUri.toString(),
203 204 205 206
          'fileContents': fileContents
        },
      );
    } catch (error) {
207
      printTrace('DevFS: Failed to write $deviceUri: $error');
208
    }
209 210
  }

211
  @override
212
  Future<dynamic> deleteFile(String fsName, Uri deviceUri) async {
213 214
    // TODO(johnmccutchan): Add file deletion to the devFS protocol.
  }
215 216
}

217
class _DevFSHttpWriter {
218
  _DevFSHttpWriter(this.fsName, VMService serviceProtocol)
219 220 221 222 223 224
      : httpAddress = serviceProtocol.httpAddress;

  final String fsName;
  final Uri httpAddress;

  static const int kMaxInFlight = 6;
225
  static const int kMaxRetries = 3;
226 227

  int _inFlight = 0;
228
  Map<Uri, DevFSContent> _outstanding;
229
  Completer<Null> _completer;
230
  HttpClient _client;
231 232 233
  int _done;
  int _max;

234
  Future<Null> write(Map<Uri, DevFSContent> entries,
235
                     {DevFSProgressReporter progressReporter}) async {
236
    _client = new HttpClient();
237 238
    _client.maxConnectionsPerHost = kMaxInFlight;
    _completer = new Completer<Null>();
239
    _outstanding = new Map<Uri, DevFSContent>.from(entries);
240 241 242 243 244 245 246 247 248
    _done = 0;
    _max = _outstanding.length;
    _scheduleWrites(progressReporter);
    await _completer.future;
    _client.close();
  }

  void _scheduleWrites(DevFSProgressReporter progressReporter) {
    while (_inFlight < kMaxInFlight) {
249
      if (_outstanding.isEmpty) {
250 251 252
        // Finished.
        break;
      }
253 254
      final Uri deviceUri = _outstanding.keys.first;
      final DevFSContent content = _outstanding.remove(deviceUri);
255
      _scheduleWrite(deviceUri, content, progressReporter);
256 257 258 259
      _inFlight++;
    }
  }

260
  Future<Null> _scheduleWrite(
261
    Uri deviceUri,
262 263 264 265
    DevFSContent content,
    DevFSProgressReporter progressReporter, [
    int retry = 0,
  ]) async {
Ryan Macnak's avatar
Ryan Macnak committed
266
    try {
267
      final HttpClientRequest request = await _client.putUrl(httpAddress);
268
      request.headers.removeAll(HttpHeaders.ACCEPT_ENCODING);
Ryan Macnak's avatar
Ryan Macnak committed
269
      request.headers.add('dev_fs_name', fsName);
270 271
      request.headers.add('dev_fs_uri_b64',
                          BASE64.encode(UTF8.encode(deviceUri.toString())));
272
      final Stream<List<int>> contents = content.contentsAsCompressedStream();
Ryan Macnak's avatar
Ryan Macnak committed
273
      await request.addStream(contents);
274
      final HttpClientResponse response = await request.close();
275
      await response.drain<Null>();
276
    } catch (e) {
277
      if (retry < kMaxRetries) {
278 279
        printTrace('Retrying writing "$deviceUri" to DevFS due to error: $e');
        _scheduleWrite(deviceUri, content, progressReporter, retry + 1);
280 281
        return;
      } else {
282
        printError('Error writing "$deviceUri" to DevFS: $e');
283
      }
Ryan Macnak's avatar
Ryan Macnak committed
284
    }
285 286 287 288 289
    if (progressReporter != null) {
      _done++;
      progressReporter(_done, _max);
    }
    _inFlight--;
290
    if ((_outstanding.isEmpty) && (_inFlight == 0)) {
291 292 293 294 295 296 297
      _completer.complete(null);
    } else {
      _scheduleWrites(progressReporter);
    }
  }
}

298 299
class DevFS {
  /// Create a [DevFS] named [fsName] for the local files in [directory].
300
  DevFS(VMService serviceProtocol,
301
        this.fsName,
302 303 304
        this.rootDirectory, {
        String packagesFilePath
      })
305
    : _operations = new ServiceProtocolDevFSOperations(serviceProtocol),
306
      _httpWriter = new _DevFSHttpWriter(fsName, serviceProtocol) {
307
    _packagesFilePath =
308
        packagesFilePath ?? fs.path.join(rootDirectory.path, kPackagesFileName);
309
  }
310 311 312

  DevFS.operations(this._operations,
                   this.fsName,
313 314 315 316 317
                   this.rootDirectory, {
                   String packagesFilePath,
      })
    : _httpWriter = null {
    _packagesFilePath =
318
        packagesFilePath ?? fs.path.join(rootDirectory.path, kPackagesFileName);
319
  }
320 321

  final DevFSOperations _operations;
322
  final _DevFSHttpWriter _httpWriter;
323 324
  final String fsName;
  final Directory rootDirectory;
325
  String _packagesFilePath;
326
  final Map<Uri, DevFSContent> _entries = <Uri, DevFSContent>{};
327
  final Set<String> assetPathsToEvict = new Set<String>();
328

329
  final List<Future<Map<String, dynamic>>> _pendingOperations =
330
      <Future<Map<String, dynamic>>>[];
331

332 333 334 335 336 337 338 339 340
  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;
  }

341
  Future<dynamic> destroy() {
342
    printTrace('DevFS: Deleted filesystem on the device ($_baseUri)');
343
    return _operations.destroy(fsName);
344 345
  }

346 347
  /// Update files on the device and return the number of bytes sync'd
  Future<int> update({ DevFSProgressReporter progressReporter,
348
                           AssetBundle bundle,
349 350
                           bool bundleDirty: false,
                           Set<String> fileFilter}) async {
351 352 353 354 355 356
    // Mark all entries as possibly deleted.
    for (DevFSContent content in _entries.values) {
      content._exists = false;
    }

    // Scan workspace, packages, and assets
357
    printTrace('DevFS: Starting sync from $rootDirectory');
358
    logger.printTrace('Scanning project files');
359
    await _scanDirectory(rootDirectory,
360 361
                         recursive: true,
                         fileFilter: fileFilter);
362
    if (fs.isFileSync(_packagesFilePath)) {
363 364
      printTrace('Scanning package files');
      await _scanPackages(fileFilter);
365
    }
366
    if (bundle != null) {
367
      printTrace('Scanning asset files');
368 369 370
      bundle.entries.forEach((String archivePath, DevFSContent content) {
        _scanBundleEntry(archivePath, content, bundleDirty);
      });
371
    }
372

373
    // Handle deletions.
374
    printTrace('Scanning for deleted files');
375
    final String assetBuildDirPrefix = _asUriPath(getAssetBuildDirectory());
376
    final List<Uri> toRemove = <Uri>[];
377
    _entries.forEach((Uri deviceUri, DevFSContent content) {
378
      if (!content._exists) {
379
        final Future<Map<String, dynamic>> operation =
380
            _operations.deleteFile(fsName, deviceUri);
381 382
        if (operation != null)
          _pendingOperations.add(operation);
383 384
        toRemove.add(deviceUri);
        if (deviceUri.path.startsWith(assetBuildDirPrefix)) {
385
          final String archivePath = deviceUri.path.substring(assetBuildDirPrefix.length);
386 387
          assetPathsToEvict.add(archivePath);
        }
388
      }
389 390 391 392
    });
    if (toRemove.isNotEmpty) {
      printTrace('Removing deleted files');
      toRemove.forEach(_entries.remove);
393 394 395
      await Future.wait(_pendingOperations);
      _pendingOperations.clear();
    }
396

397 398
    // Update modified files
    int numBytes = 0;
399
    final Map<Uri, DevFSContent> dirtyEntries = <Uri, DevFSContent>{};
400
    _entries.forEach((Uri deviceUri, DevFSContent content) {
401
      String archivePath;
402 403
      if (deviceUri.path.startsWith(assetBuildDirPrefix))
        archivePath = deviceUri.path.substring(assetBuildDirPrefix.length);
404
      if (content.isModified || (bundleDirty && archivePath != null)) {
405
        dirtyEntries[deviceUri] = content;
406 407 408 409 410
        numBytes += content.size;
        if (archivePath != null)
          assetPathsToEvict.add(archivePath);
      }
    });
411
    if (dirtyEntries.isNotEmpty) {
412
      printTrace('Updating files');
413
      if (_httpWriter != null) {
414
        try {
415
          await _httpWriter.write(dirtyEntries,
416 417 418 419
                                  progressReporter: progressReporter);
        } catch (e) {
          printError("Could not update files on device: $e");
        }
420 421
      } else {
        // Make service protocol requests for each.
422
        dirtyEntries.forEach((Uri deviceUri, DevFSContent content) {
423
          final Future<Map<String, dynamic>> operation =
424
              _operations.writeFile(fsName, deviceUri, content);
425 426
          if (operation != null)
            _pendingOperations.add(operation);
427
        });
428 429 430
        if (progressReporter != null) {
          final int max = _pendingOperations.length;
          int complete = 0;
431 432 433 434
          _pendingOperations.forEach((Future<dynamic> f) => f.whenComplete(() {
            // TODO(ianh): If one of the pending operations fail, we'll keep
            // calling progressReporter long after update() has completed its
            // future, assuming that doesn't crash the app.
435 436 437 438 439 440 441
            complete += 1;
            progressReporter(complete, max);
          }));
        }
        await Future.wait(_pendingOperations, eagerError: true);
        _pendingOperations.clear();
      }
442 443
    }

444
    printTrace('DevFS: Sync finished');
445
    return numBytes;
446 447
  }

448
  void _scanFile(Uri deviceUri, FileSystemEntity file) {
449
    final DevFSContent content = _entries.putIfAbsent(deviceUri, () => new DevFSFileContent(file));
450
    content._exists = true;
451 452
  }

453 454 455
  void _scanBundleEntry(String archivePath, DevFSContent content, bool bundleDirty) {
    // We write the assets into the AssetBundle working dir so that they
    // are in the same location in DevFS and the iOS simulator.
456
    final Uri deviceUri = fs.path.toUri(fs.path.join(getAssetBuildDirectory(), archivePath));
457

458
    _entries[deviceUri] = content;
459
    content._exists = true;
460 461
  }

462
  bool _shouldIgnore(Uri deviceUri) {
463
    final List<String> ignoredUriPrefixes = <String>['android/',
464 465 466 467 468
                                               _asUriPath(getBuildDirectory()),
                                               'ios/',
                                               '.pub/'];
    for (String ignoredUriPrefix in ignoredUriPrefixes) {
      if (deviceUri.path.startsWith(ignoredUriPrefix))
469 470 471 472 473
        return true;
    }
    return false;
  }

474
  Future<bool> _scanDirectory(Directory directory,
475
                              {Uri directoryUriOnDevice,
476
                               bool recursive: false,
477 478
                               bool ignoreDotFiles: true,
                               Set<String> fileFilter}) async {
479
    if (directoryUriOnDevice == null) {
480
      final String relativeRootPath = fs.path.relative(directory.path, from: rootDirectory.path);
481 482 483 484 485
      if (relativeRootPath == '.') {
        directoryUriOnDevice = new Uri();
      } else {
        directoryUriOnDevice = fs.path.toUri(relativeRootPath);
      }
486 487
    }
    try {
488
      final Stream<FileSystemEntity> files =
489 490
          directory.list(recursive: recursive, followLinks: false);
      await for (FileSystemEntity file in files) {
491 492
        if (!devFSConfig.noDirectorySymlinks && (file is Link)) {
          // Check if this is a symlink to a directory and skip it.
493 494
          final String linkPath = file.resolveSymbolicLinksSync();
          final FileSystemEntityType linkType =
495
              fs.statSync(linkPath).type;
496 497 498 499 500
          if (linkType == FileSystemEntityType.DIRECTORY) {
            continue;
          }
        }
        if (file is Directory) {
501 502 503
          // Skip non-files.
          continue;
        }
504
        assert((file is Link) || (file is File));
505
        if (ignoreDotFiles && fs.path.basename(file.path).startsWith('.')) {
506 507 508
          // Skip dot files.
          continue;
        }
509
        final String relativePath =
510
            fs.path.relative(file.path, from: directory.path);
511
        final Uri deviceUri = directoryUriOnDevice.resolveUri(fs.path.toUri(relativePath));
512 513
        final String canonicalizeFilePath = fs.path.canonicalize(file.absolute.path);
        if ((fileFilter != null) && !fileFilter.contains(canonicalizeFilePath)) {
514 515 516
          // Skip files that are not included in the filter.
          continue;
        }
517
        if (ignoreDotFiles && deviceUri.path.startsWith('.')) {
518 519 520
          // Skip directories that start with a dot.
          continue;
        }
521 522
        if (!_shouldIgnore(deviceUri))
          _scanFile(deviceUri, file);
523 524 525 526 527 528 529
      }
    } catch (e) {
      // Ignore directory and error.
      return false;
    }
    return true;
  }
530 531 532

  Future<Null> _scanPackages(Set<String> fileFilter) async {
    StringBuffer sb;
533
    final PackageMap packageMap = new PackageMap(_packagesFilePath);
534 535

    for (String packageName in packageMap.map.keys) {
536 537 538
      final Uri packageUri = packageMap.map[packageName];
      final String packagePath = packageUri.toFilePath();
      final Directory packageDirectory = fs.directory(packageUri);
539
      Uri directoryUriOnDevice = fs.path.toUri(fs.path.join('packages', packageName) + fs.path.separator);
540 541 542 543 544
      bool packageExists;

      if (fs.path.isWithin(rootDirectory.path, packagePath)) {
        // We already scanned everything under the root directory.
        packageExists = packageDirectory.existsSync();
545 546 547
        directoryUriOnDevice = fs.path.toUri(
            fs.path.relative(packagePath, from: rootDirectory.path) + fs.path.separator
        );
548 549 550
      } else {
        packageExists =
            await _scanDirectory(packageDirectory,
551
                                 directoryUriOnDevice: directoryUriOnDevice,
552 553 554
                                 recursive: true,
                                 fileFilter: fileFilter);
      }
555 556
      if (packageExists) {
        sb ??= new StringBuffer();
557
        sb.writeln('$packageName:$directoryUriOnDevice');
558 559 560
      }
    }
    if (sb != null) {
561
      final DevFSContent content = _entries[fs.path.toUri('.packages')];
562 563 564 565
      if (content is DevFSStringContent && content.string == sb.toString()) {
        content._exists = true;
        return;
      }
566
      _entries[fs.path.toUri('.packages')] = new DevFSStringContent(sb.toString());
567 568
    }
  }
569
}
570 571
/// Converts a platform-specific file path to a platform-independent Uri path.
String _asUriPath(String filePath) => fs.path.toUri(filePath).path + '/';