• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2016 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6
7import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
8import 'package:meta/meta.dart';
9
10import 'asset.dart';
11import 'base/context.dart';
12import 'base/file_system.dart';
13import 'base/io.dart';
14import 'build_info.dart';
15import 'bundle.dart';
16import 'compile.dart';
17import 'convert.dart' show base64, utf8;
18import 'dart/package_map.dart';
19import 'globals.dart';
20import 'vmservice.dart';
21
22class DevFSConfig {
23  /// Should DevFS assume that symlink targets are stable?
24  bool cacheSymlinks = false;
25  /// Should DevFS assume that there are no symlinks to directories?
26  bool noDirectorySymlinks = false;
27}
28
29DevFSConfig get devFSConfig => context.get<DevFSConfig>();
30
31/// Common superclass for content copied to the device.
32abstract class DevFSContent {
33  /// Return true if this is the first time this method is called
34  /// or if the entry has been modified since this method was last called.
35  bool get isModified;
36
37  /// Return true if this is the first time this method is called
38  /// or if the entry has been modified after the given time
39  /// or if the given time is null.
40  bool isModifiedAfter(DateTime time);
41
42  int get size;
43
44  Future<List<int>> contentsAsBytes();
45
46  Stream<List<int>> contentsAsStream();
47
48  Stream<List<int>> contentsAsCompressedStream() {
49    return contentsAsStream().cast<List<int>>().transform<List<int>>(gzip.encoder);
50  }
51
52  /// Return the list of files this content depends on.
53  List<String> get fileDependencies => <String>[];
54}
55
56// File content to be copied to the device.
57class DevFSFileContent extends DevFSContent {
58  DevFSFileContent(this.file);
59
60  final FileSystemEntity file;
61  FileSystemEntity _linkTarget;
62  FileStat _fileStat;
63
64  File _getFile() {
65    if (_linkTarget != null) {
66      return _linkTarget;
67    }
68    if (file is Link) {
69      // The link target.
70      return fs.file(file.resolveSymbolicLinksSync());
71    }
72    return file;
73  }
74
75  void _stat() {
76    if (_linkTarget != null) {
77      // Stat the cached symlink target.
78      final FileStat fileStat = _linkTarget.statSync();
79      if (fileStat.type == FileSystemEntityType.notFound) {
80        _linkTarget = null;
81      } else {
82        _fileStat = fileStat;
83        return;
84      }
85    }
86    final FileStat fileStat = file.statSync();
87    _fileStat = fileStat.type == FileSystemEntityType.notFound ? null : fileStat;
88    if (_fileStat != null && _fileStat.type == FileSystemEntityType.link) {
89      // Resolve, stat, and maybe cache the symlink target.
90      final String resolved = file.resolveSymbolicLinksSync();
91      final FileSystemEntity linkTarget = fs.file(resolved);
92      // Stat the link target.
93      final FileStat fileStat = linkTarget.statSync();
94      if (fileStat.type == FileSystemEntityType.notFound) {
95        _fileStat = null;
96        _linkTarget = null;
97      } else if (devFSConfig.cacheSymlinks) {
98        _linkTarget = linkTarget;
99      }
100    }
101    if (_fileStat == null) {
102      printError('Unable to get status of file "${file.path}": file not found.');
103    }
104  }
105
106  @override
107  List<String> get fileDependencies => <String>[_getFile().path];
108
109  @override
110  bool get isModified {
111    final FileStat _oldFileStat = _fileStat;
112    _stat();
113    if (_oldFileStat == null && _fileStat == null)
114      return false;
115    return _oldFileStat == null || _fileStat == null || _fileStat.modified.isAfter(_oldFileStat.modified);
116  }
117
118  @override
119  bool isModifiedAfter(DateTime time) {
120    final FileStat _oldFileStat = _fileStat;
121    _stat();
122    if (_oldFileStat == null && _fileStat == null)
123      return false;
124    return time == null
125        || _oldFileStat == null
126        || _fileStat == null
127        || _fileStat.modified.isAfter(time);
128  }
129
130  @override
131  int get size {
132    if (_fileStat == null)
133      _stat();
134    // Can still be null if the file wasn't found.
135    return _fileStat?.size ?? 0;
136  }
137
138  @override
139  Future<List<int>> contentsAsBytes() => _getFile().readAsBytes();
140
141  @override
142  Stream<List<int>> contentsAsStream() => _getFile().openRead();
143}
144
145/// Byte content to be copied to the device.
146class DevFSByteContent extends DevFSContent {
147  DevFSByteContent(this._bytes);
148
149  List<int> _bytes;
150
151  bool _isModified = true;
152  DateTime _modificationTime = DateTime.now();
153
154  List<int> get bytes => _bytes;
155
156  set bytes(List<int> value) {
157    _bytes = value;
158    _isModified = true;
159    _modificationTime = DateTime.now();
160  }
161
162  /// Return true only once so that the content is written to the device only once.
163  @override
164  bool get isModified {
165    final bool modified = _isModified;
166    _isModified = false;
167    return modified;
168  }
169
170  @override
171  bool isModifiedAfter(DateTime time) {
172    return time == null || _modificationTime.isAfter(time);
173  }
174
175  @override
176  int get size => _bytes.length;
177
178  @override
179  Future<List<int>> contentsAsBytes() async => _bytes;
180
181  @override
182  Stream<List<int>> contentsAsStream() =>
183      Stream<List<int>>.fromIterable(<List<int>>[_bytes]);
184}
185
186/// String content to be copied to the device.
187class DevFSStringContent extends DevFSByteContent {
188  DevFSStringContent(String string)
189    : _string = string,
190      super(utf8.encode(string));
191
192  String _string;
193
194  String get string => _string;
195
196  set string(String value) {
197    _string = value;
198    super.bytes = utf8.encode(_string);
199  }
200
201  @override
202  set bytes(List<int> value) {
203    string = utf8.decode(value);
204  }
205}
206
207/// Abstract DevFS operations interface.
208abstract class DevFSOperations {
209  Future<Uri> create(String fsName);
210  Future<dynamic> destroy(String fsName);
211  Future<dynamic> writeFile(String fsName, Uri deviceUri, DevFSContent content);
212}
213
214/// An implementation of [DevFSOperations] that speaks to the
215/// vm service.
216class ServiceProtocolDevFSOperations implements DevFSOperations {
217  ServiceProtocolDevFSOperations(this.vmService);
218
219  final VMService vmService;
220
221  @override
222  Future<Uri> create(String fsName) async {
223    final Map<String, dynamic> response = await vmService.vm.createDevFS(fsName);
224    return Uri.parse(response['uri']);
225  }
226
227  @override
228  Future<dynamic> destroy(String fsName) async {
229    await vmService.vm.deleteDevFS(fsName);
230  }
231
232  @override
233  Future<dynamic> writeFile(String fsName, Uri deviceUri, DevFSContent content) async {
234    List<int> bytes;
235    try {
236      bytes = await content.contentsAsBytes();
237    } catch (e) {
238      return e;
239    }
240    final String fileContents = base64.encode(bytes);
241    try {
242      return await vmService.vm.invokeRpcRaw(
243        '_writeDevFSFile',
244        params: <String, dynamic>{
245          'fsName': fsName,
246          'uri': deviceUri.toString(),
247          'fileContents': fileContents,
248        },
249      );
250    } catch (error) {
251      printTrace('DevFS: Failed to write $deviceUri: $error');
252    }
253  }
254}
255
256class DevFSException implements Exception {
257  DevFSException(this.message, [this.error, this.stackTrace]);
258  final String message;
259  final dynamic error;
260  final StackTrace stackTrace;
261}
262
263class _DevFSHttpWriter {
264  _DevFSHttpWriter(this.fsName, VMService serviceProtocol)
265    : httpAddress = serviceProtocol.httpAddress;
266
267  final String fsName;
268  final Uri httpAddress;
269
270  static const int kMaxInFlight = 6;
271
272  int _inFlight = 0;
273  Map<Uri, DevFSContent> _outstanding;
274  Completer<void> _completer;
275  final HttpClient _client = HttpClient();
276
277  Future<void> write(Map<Uri, DevFSContent> entries) async {
278    _client.maxConnectionsPerHost = kMaxInFlight;
279    _completer = Completer<void>();
280    _outstanding = Map<Uri, DevFSContent>.from(entries);
281    _scheduleWrites();
282    await _completer.future;
283  }
284
285  void _scheduleWrites() {
286    while ((_inFlight < kMaxInFlight) && (!_completer.isCompleted) && _outstanding.isNotEmpty) {
287      final Uri deviceUri = _outstanding.keys.first;
288      final DevFSContent content = _outstanding.remove(deviceUri);
289      _startWrite(deviceUri, content);
290      _inFlight += 1;
291    }
292    if ((_inFlight == 0) && (!_completer.isCompleted) && _outstanding.isEmpty)
293      _completer.complete();
294  }
295
296  Future<void> _startWrite(
297    Uri deviceUri,
298    DevFSContent content, [
299    int retry = 0,
300  ]) async {
301    try {
302      final HttpClientRequest request = await _client.putUrl(httpAddress);
303      request.headers.removeAll(HttpHeaders.acceptEncodingHeader);
304      request.headers.add('dev_fs_name', fsName);
305      request.headers.add('dev_fs_uri_b64', base64.encode(utf8.encode('$deviceUri')));
306      final Stream<List<int>> contents = content.contentsAsCompressedStream();
307      await request.addStream(contents);
308      final HttpClientResponse response = await request.close();
309      await response.drain<void>();
310    } catch (error, trace) {
311      if (!_completer.isCompleted) {
312        printTrace('Error writing "$deviceUri" to DevFS: $error');
313        _completer.completeError(error, trace);
314      }
315    }
316    _inFlight -= 1;
317    _scheduleWrites();
318  }
319}
320
321// Basic statistics for DevFS update operation.
322class UpdateFSReport {
323  UpdateFSReport({
324    bool success = false,
325    int invalidatedSourcesCount = 0,
326    int syncedBytes = 0,
327  }) {
328    _success = success;
329    _invalidatedSourcesCount = invalidatedSourcesCount;
330    _syncedBytes = syncedBytes;
331  }
332
333  bool get success => _success;
334  int get invalidatedSourcesCount => _invalidatedSourcesCount;
335  int get syncedBytes => _syncedBytes;
336
337  void incorporateResults(UpdateFSReport report) {
338    if (!report._success) {
339      _success = false;
340    }
341    _invalidatedSourcesCount += report._invalidatedSourcesCount;
342    _syncedBytes += report._syncedBytes;
343  }
344
345  bool _success;
346  int _invalidatedSourcesCount;
347  int _syncedBytes;
348}
349
350class DevFS {
351  /// Create a [DevFS] named [fsName] for the local files in [rootDirectory].
352  DevFS(
353    VMService serviceProtocol,
354    this.fsName,
355    this.rootDirectory, {
356    String packagesFilePath,
357  }) : _operations = ServiceProtocolDevFSOperations(serviceProtocol),
358       _httpWriter = _DevFSHttpWriter(fsName, serviceProtocol),
359       _packagesFilePath = packagesFilePath ?? fs.path.join(rootDirectory.path, kPackagesFileName);
360
361  DevFS.operations(
362    this._operations,
363    this.fsName,
364    this.rootDirectory, {
365    String packagesFilePath,
366  }) : _httpWriter = null,
367       _packagesFilePath = packagesFilePath ?? fs.path.join(rootDirectory.path, kPackagesFileName);
368
369  final DevFSOperations _operations;
370  final _DevFSHttpWriter _httpWriter;
371  final String fsName;
372  final Directory rootDirectory;
373  final String _packagesFilePath;
374  final Set<String> assetPathsToEvict = <String>{};
375  List<Uri> sources = <Uri>[];
376  DateTime lastCompiled;
377
378  Uri _baseUri;
379  Uri get baseUri => _baseUri;
380
381  Uri deviceUriToHostUri(Uri deviceUri) {
382    final String deviceUriString = deviceUri.toString();
383    final String baseUriString = baseUri.toString();
384    if (deviceUriString.startsWith(baseUriString)) {
385      final String deviceUriSuffix = deviceUriString.substring(baseUriString.length);
386      return rootDirectory.uri.resolve(deviceUriSuffix);
387    }
388    return deviceUri;
389  }
390
391  Future<Uri> create() async {
392    printTrace('DevFS: Creating new filesystem on the device ($_baseUri)');
393    try {
394      _baseUri = await _operations.create(fsName);
395    } on rpc.RpcException catch (rpcException) {
396      // 1001 is kFileSystemAlreadyExists in //dart/runtime/vm/json_stream.h
397      if (rpcException.code != 1001)
398        rethrow;
399      printTrace('DevFS: Creating failed. Destroying and trying again');
400      await destroy();
401      _baseUri = await _operations.create(fsName);
402    }
403    printTrace('DevFS: Created new filesystem on the device ($_baseUri)');
404    return _baseUri;
405  }
406
407  Future<void> destroy() async {
408    printTrace('DevFS: Deleting filesystem on the device ($_baseUri)');
409    await _operations.destroy(fsName);
410    printTrace('DevFS: Deleted filesystem on the device ($_baseUri)');
411  }
412
413  /// Updates files on the device.
414  ///
415  /// Returns the number of bytes synced.
416  Future<UpdateFSReport> update({
417    @required String mainPath,
418    String target,
419    AssetBundle bundle,
420    DateTime firstBuildTime,
421    bool bundleFirstUpload = false,
422    @required ResidentCompiler generator,
423    String dillOutputPath,
424    @required bool trackWidgetCreation,
425    bool fullRestart = false,
426    String projectRootPath,
427    @required String pathToReload,
428    @required List<Uri> invalidatedFiles,
429  }) async {
430    assert(trackWidgetCreation != null);
431    assert(generator != null);
432
433    // Update modified files
434    final String assetBuildDirPrefix = _asUriPath(getAssetBuildDirectory());
435    final Map<Uri, DevFSContent> dirtyEntries = <Uri, DevFSContent>{};
436
437    int syncedBytes = 0;
438    if (bundle != null) {
439      printTrace('Scanning asset files');
440      // We write the assets into the AssetBundle working dir so that they
441      // are in the same location in DevFS and the iOS simulator.
442      final String assetDirectory = getAssetBuildDirectory();
443      bundle.entries.forEach((String archivePath, DevFSContent content) {
444        final Uri deviceUri = fs.path.toUri(fs.path.join(assetDirectory, archivePath));
445        if (deviceUri.path.startsWith(assetBuildDirPrefix)) {
446          archivePath = deviceUri.path.substring(assetBuildDirPrefix.length);
447        }
448        // Only update assets if they have been modified, or if this is the
449        // first upload of the asset bundle.
450        if (content.isModified || (bundleFirstUpload && archivePath != null)) {
451          dirtyEntries[deviceUri] = content;
452          syncedBytes += content.size;
453          if (archivePath != null && !bundleFirstUpload) {
454            assetPathsToEvict.add(archivePath);
455          }
456        }
457      });
458    }
459    if (fullRestart) {
460      generator.reset();
461    }
462    printTrace('Compiling dart to kernel with ${invalidatedFiles.length} updated files');
463    lastCompiled = DateTime.now();
464    final CompilerOutput compilerOutput = await generator.recompile(
465      mainPath,
466      invalidatedFiles,
467      outputPath:  dillOutputPath ?? getDefaultApplicationKernelPath(trackWidgetCreation: trackWidgetCreation),
468      packagesFilePath : _packagesFilePath,
469    );
470    if (compilerOutput == null || compilerOutput.errorCount > 0) {
471      return UpdateFSReport(success: false);
472    }
473    // list of sources that needs to be monitored are in [compilerOutput.sources]
474    sources = compilerOutput.sources;
475    //
476    // Don't send full kernel file that would overwrite what VM already
477    // started loading from.
478    if (!bundleFirstUpload) {
479      final String compiledBinary = compilerOutput?.outputFilename;
480      if (compiledBinary != null && compiledBinary.isNotEmpty) {
481        final Uri entryUri = fs.path.toUri(projectRootPath != null
482          ? fs.path.relative(pathToReload, from: projectRootPath)
483          : pathToReload,
484        );
485        final DevFSFileContent content = DevFSFileContent(fs.file(compiledBinary));
486        syncedBytes += content.size;
487        dirtyEntries[entryUri] = content;
488      }
489    }
490    printTrace('Updating files');
491    if (dirtyEntries.isNotEmpty) {
492      try {
493        await _httpWriter.write(dirtyEntries);
494      } on SocketException catch (socketException, stackTrace) {
495        printTrace('DevFS sync failed. Lost connection to device: $socketException');
496        throw DevFSException('Lost connection to device.', socketException, stackTrace);
497      } catch (exception, stackTrace) {
498        printError('Could not update files on device: $exception');
499        throw DevFSException('Sync failed', exception, stackTrace);
500      }
501    }
502    printTrace('DevFS: Sync finished');
503    return UpdateFSReport(success: true, syncedBytes: syncedBytes,
504         invalidatedSourcesCount: invalidatedFiles.length);
505  }
506}
507
508/// Converts a platform-specific file path to a platform-independent Uri path.
509String _asUriPath(String filePath) => fs.path.toUri(filePath).path + '/';
510