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