Skip to content

Commit 17057bb

Browse files
authored
[devFS] Use URI to represent paths on device (flutter#8446)
* [devFS] Use URI to represent paths on device Previosuly, regular file paths in the format of the host platform were used to represent paths on device. That works when host and device share the same (POSIX) file path format. With a Windows host, this breaks. URIs are the solution as they are platform independent and the VM service on the device already interpreted the file paths as URIs anyways. * review comments * switch to file paths * fix tests on Windows * review comments
1 parent 5ce67b0 commit 17057bb

File tree

5 files changed

+110
-100
lines changed

5 files changed

+110
-100
lines changed

packages/flutter_tools/lib/src/devfs.dart

Lines changed: 68 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,8 @@ class DevFSStringContent extends DevFSByteContent {
160160
abstract class DevFSOperations {
161161
Future<Uri> create(String fsName);
162162
Future<dynamic> destroy(String fsName);
163-
Future<dynamic> writeFile(String fsName, String devicePath, DevFSContent content);
164-
Future<dynamic> deleteFile(String fsName, String devicePath);
163+
Future<dynamic> writeFile(String fsName, Uri deviceUri, DevFSContent content);
164+
Future<dynamic> deleteFile(String fsName, Uri deviceUri);
165165
}
166166

167167
/// An implementation of [DevFSOperations] that speaks to the
@@ -186,7 +186,7 @@ class ServiceProtocolDevFSOperations implements DevFSOperations {
186186
}
187187

188188
@override
189-
Future<dynamic> writeFile(String fsName, String devicePath, DevFSContent content) async {
189+
Future<dynamic> writeFile(String fsName, Uri deviceUri, DevFSContent content) async {
190190
List<int> bytes;
191191
try {
192192
bytes = await content.contentsAsBytes();
@@ -199,17 +199,18 @@ class ServiceProtocolDevFSOperations implements DevFSOperations {
199199
'_writeDevFSFile',
200200
params: <String, dynamic> {
201201
'fsName': fsName,
202-
'path': devicePath,
202+
// TODO(goderbauer): transfer real Uri (instead of file path) when remote end supports it
203+
'path': deviceUri.toFilePath(windows: false),
203204
'fileContents': fileContents
204205
},
205206
);
206207
} catch (error) {
207-
printTrace('DevFS: Failed to write $devicePath: $error');
208+
printTrace('DevFS: Failed to write $deviceUri: $error');
208209
}
209210
}
210211

211212
@override
212-
Future<dynamic> deleteFile(String fsName, String devicePath) async {
213+
Future<dynamic> deleteFile(String fsName, Uri deviceUri) async {
213214
// TODO(johnmccutchan): Add file deletion to the devFS protocol.
214215
}
215216
}
@@ -225,18 +226,18 @@ class _DevFSHttpWriter {
225226
static const int kMaxRetries = 3;
226227

227228
int _inFlight = 0;
228-
Map<String, DevFSContent> _outstanding;
229+
Map<Uri, DevFSContent> _outstanding;
229230
Completer<Null> _completer;
230231
HttpClient _client;
231232
int _done;
232233
int _max;
233234

234-
Future<Null> write(Map<String, DevFSContent> entries,
235+
Future<Null> write(Map<Uri, DevFSContent> entries,
235236
{DevFSProgressReporter progressReporter}) async {
236237
_client = new HttpClient();
237238
_client.maxConnectionsPerHost = kMaxInFlight;
238239
_completer = new Completer<Null>();
239-
_outstanding = new Map<String, DevFSContent>.from(entries);
240+
_outstanding = new Map<Uri, DevFSContent>.from(entries);
240241
_done = 0;
241242
_max = _outstanding.length;
242243
_scheduleWrites(progressReporter);
@@ -250,15 +251,15 @@ class _DevFSHttpWriter {
250251
// Finished.
251252
break;
252253
}
253-
String devicePath = _outstanding.keys.first;
254-
DevFSContent content = _outstanding.remove(devicePath);
255-
_scheduleWrite(devicePath, content, progressReporter);
254+
Uri deviceUri = _outstanding.keys.first;
255+
DevFSContent content = _outstanding.remove(deviceUri);
256+
_scheduleWrite(deviceUri, content, progressReporter);
256257
_inFlight++;
257258
}
258259
}
259260

260261
Future<Null> _scheduleWrite(
261-
String devicePath,
262+
Uri deviceUri,
262263
DevFSContent content,
263264
DevFSProgressReporter progressReporter, [
264265
int retry = 0,
@@ -267,19 +268,20 @@ class _DevFSHttpWriter {
267268
HttpClientRequest request = await _client.putUrl(httpAddress);
268269
request.headers.removeAll(HttpHeaders.ACCEPT_ENCODING);
269270
request.headers.add('dev_fs_name', fsName);
271+
// TODO(goderbauer): transfer real Uri (instead of file path) when remote end supports it
270272
request.headers.add('dev_fs_path_b64',
271-
BASE64.encode(UTF8.encode(devicePath)));
273+
BASE64.encode(UTF8.encode(deviceUri.toFilePath(windows: false))));
272274
Stream<List<int>> contents = content.contentsAsCompressedStream();
273275
await request.addStream(contents);
274276
HttpClientResponse response = await request.close();
275277
await response.drain<Null>();
276278
} catch (e) {
277279
if (retry < kMaxRetries) {
278-
printTrace('Retrying writing "$devicePath" to DevFS due to error: $e');
279-
_scheduleWrite(devicePath, content, progressReporter, retry + 1);
280+
printTrace('Retrying writing "$deviceUri" to DevFS due to error: $e');
281+
_scheduleWrite(deviceUri, content, progressReporter, retry + 1);
280282
return;
281283
} else {
282-
printError('Error writing "$devicePath" to DevFS: $e');
284+
printError('Error writing "$deviceUri" to DevFS: $e');
283285
}
284286
}
285287
if (progressReporter != null) {
@@ -324,7 +326,7 @@ class DevFS {
324326
final String fsName;
325327
final Directory rootDirectory;
326328
String _packagesFilePath;
327-
final Map<String, DevFSContent> _entries = <String, DevFSContent>{};
329+
final Map<Uri, DevFSContent> _entries = <Uri, DevFSContent>{};
328330
final Set<String> assetPathsToEvict = new Set<String>();
329331

330332
final List<Future<Map<String, dynamic>>> _pendingOperations =
@@ -373,17 +375,17 @@ class DevFS {
373375

374376
// Handle deletions.
375377
printTrace('Scanning for deleted files');
376-
String assetBuildDirPrefix = getAssetBuildDirectory() + fs.path.separator;
377-
final List<String> toRemove = new List<String>();
378-
_entries.forEach((String devicePath, DevFSContent content) {
378+
String assetBuildDirPrefix = _asUriPath(getAssetBuildDirectory());
379+
final List<Uri> toRemove = new List<Uri>();
380+
_entries.forEach((Uri deviceUri, DevFSContent content) {
379381
if (!content._exists) {
380382
Future<Map<String, dynamic>> operation =
381-
_operations.deleteFile(fsName, devicePath);
383+
_operations.deleteFile(fsName, deviceUri);
382384
if (operation != null)
383385
_pendingOperations.add(operation);
384-
toRemove.add(devicePath);
385-
if (devicePath.startsWith(assetBuildDirPrefix)) {
386-
String archivePath = devicePath.substring(assetBuildDirPrefix.length);
386+
toRemove.add(deviceUri);
387+
if (deviceUri.path.startsWith(assetBuildDirPrefix)) {
388+
String archivePath = deviceUri.path.substring(assetBuildDirPrefix.length);
387389
assetPathsToEvict.add(archivePath);
388390
}
389391
}
@@ -397,13 +399,13 @@ class DevFS {
397399

398400
// Update modified files
399401
int numBytes = 0;
400-
Map<String, DevFSContent> dirtyEntries = <String, DevFSContent>{};
401-
_entries.forEach((String devicePath, DevFSContent content) {
402+
Map<Uri, DevFSContent> dirtyEntries = <Uri, DevFSContent>{};
403+
_entries.forEach((Uri deviceUri, DevFSContent content) {
402404
String archivePath;
403-
if (devicePath.startsWith(assetBuildDirPrefix))
404-
archivePath = devicePath.substring(assetBuildDirPrefix.length);
405+
if (deviceUri.path.startsWith(assetBuildDirPrefix))
406+
archivePath = deviceUri.path.substring(assetBuildDirPrefix.length);
405407
if (content.isModified || (bundleDirty && archivePath != null)) {
406-
dirtyEntries[devicePath] = content;
408+
dirtyEntries[deviceUri] = content;
407409
numBytes += content.size;
408410
if (archivePath != null)
409411
assetPathsToEvict.add(archivePath);
@@ -420,9 +422,9 @@ class DevFS {
420422
}
421423
} else {
422424
// Make service protocol requests for each.
423-
dirtyEntries.forEach((String devicePath, DevFSContent content) {
425+
dirtyEntries.forEach((Uri deviceUri, DevFSContent content) {
424426
Future<Map<String, dynamic>> operation =
425-
_operations.writeFile(fsName, devicePath, content);
427+
_operations.writeFile(fsName, deviceUri, content);
426428
if (operation != null)
427429
_pendingOperations.add(operation);
428430
});
@@ -446,41 +448,44 @@ class DevFS {
446448
return numBytes;
447449
}
448450

449-
void _scanFile(String devicePath, FileSystemEntity file) {
450-
DevFSContent content = _entries.putIfAbsent(devicePath, () => new DevFSFileContent(file));
451+
void _scanFile(Uri deviceUri, FileSystemEntity file) {
452+
DevFSContent content = _entries.putIfAbsent(deviceUri, () => new DevFSFileContent(file));
451453
content._exists = true;
452454
}
453455

454456
void _scanBundleEntry(String archivePath, DevFSContent content, bool bundleDirty) {
455457
// We write the assets into the AssetBundle working dir so that they
456458
// are in the same location in DevFS and the iOS simulator.
457-
final String devicePath = fs.path.join(getAssetBuildDirectory(), archivePath);
459+
final Uri deviceUri = fs.path.toUri(fs.path.join(getAssetBuildDirectory(), archivePath));
458460

459-
_entries[devicePath] = content;
461+
_entries[deviceUri] = content;
460462
content._exists = true;
461463
}
462464

463-
bool _shouldIgnore(String devicePath) {
464-
List<String> ignoredPrefixes = <String>['android' + fs.path.separator,
465-
getBuildDirectory(),
466-
'ios' + fs.path.separator,
467-
'.pub' + fs.path.separator];
468-
for (String ignoredPrefix in ignoredPrefixes) {
469-
if (devicePath.startsWith(ignoredPrefix))
465+
bool _shouldIgnore(Uri deviceUri) {
466+
List<String> ignoredUriPrefixes = <String>['android/',
467+
_asUriPath(getBuildDirectory()),
468+
'ios/',
469+
'.pub/'];
470+
for (String ignoredUriPrefix in ignoredUriPrefixes) {
471+
if (deviceUri.path.startsWith(ignoredUriPrefix))
470472
return true;
471473
}
472474
return false;
473475
}
474476

475477
Future<bool> _scanDirectory(Directory directory,
476-
{String directoryNameOnDevice,
478+
{Uri directoryUriOnDevice,
477479
bool recursive: false,
478480
bool ignoreDotFiles: true,
479481
Set<String> fileFilter}) async {
480-
if (directoryNameOnDevice == null) {
481-
directoryNameOnDevice = fs.path.relative(directory.path, from: rootDirectory.path);
482-
if (directoryNameOnDevice == '.')
483-
directoryNameOnDevice = '';
482+
if (directoryUriOnDevice == null) {
483+
String relativeRootPath = fs.path.relative(directory.path, from: rootDirectory.path);
484+
if (relativeRootPath == '.') {
485+
directoryUriOnDevice = new Uri();
486+
} else {
487+
directoryUriOnDevice = fs.path.toUri(relativeRootPath);
488+
}
484489
}
485490
try {
486491
Stream<FileSystemEntity> files =
@@ -506,17 +511,17 @@ class DevFS {
506511
}
507512
final String relativePath =
508513
fs.path.relative(file.path, from: directory.path);
509-
final String devicePath = fs.path.join(directoryNameOnDevice, relativePath);
514+
final Uri deviceUri = directoryUriOnDevice.resolveUri(fs.path.toUri(relativePath));
510515
if ((fileFilter != null) && !fileFilter.contains(file.absolute.path)) {
511516
// Skip files that are not included in the filter.
512517
continue;
513518
}
514-
if (ignoreDotFiles && devicePath.startsWith('.')) {
519+
if (ignoreDotFiles && deviceUri.path.startsWith('.')) {
515520
// Skip directories that start with a dot.
516521
continue;
517522
}
518-
if (!_shouldIgnore(devicePath))
519-
_scanFile(devicePath, file);
523+
if (!_shouldIgnore(deviceUri))
524+
_scanFile(deviceUri, file);
520525
}
521526
} catch (e) {
522527
// Ignore directory and error.
@@ -531,34 +536,38 @@ class DevFS {
531536

532537
for (String packageName in packageMap.map.keys) {
533538
Uri packageUri = packageMap.map[packageName];
534-
String packagePath = fs.path.fromUri(packageUri);
539+
String packagePath = packageUri.toFilePath();
535540
Directory packageDirectory = fs.directory(packageUri);
536-
String directoryNameOnDevice = fs.path.join('packages', packageName);
541+
Uri directoryUriOnDevice = fs.path.toUri(fs.path.join('packages', packageName) + fs.path.separator);
537542
bool packageExists;
538543

539544
if (fs.path.isWithin(rootDirectory.path, packagePath)) {
540545
// We already scanned everything under the root directory.
541546
packageExists = packageDirectory.existsSync();
542-
directoryNameOnDevice = fs.path.relative(packagePath, from: rootDirectory.path);
547+
directoryUriOnDevice = fs.path.toUri(
548+
fs.path.relative(packagePath, from: rootDirectory.path) + fs.path.separator
549+
);
543550
} else {
544551
packageExists =
545552
await _scanDirectory(packageDirectory,
546-
directoryNameOnDevice: directoryNameOnDevice,
553+
directoryUriOnDevice: directoryUriOnDevice,
547554
recursive: true,
548555
fileFilter: fileFilter);
549556
}
550557
if (packageExists) {
551558
sb ??= new StringBuffer();
552-
sb.writeln('$packageName:$directoryNameOnDevice');
559+
sb.writeln('$packageName:$directoryUriOnDevice');
553560
}
554561
}
555562
if (sb != null) {
556-
DevFSContent content = _entries['.packages'];
563+
DevFSContent content = _entries[fs.path.toUri('.packages')];
557564
if (content is DevFSStringContent && content.string == sb.toString()) {
558565
content._exists = true;
559566
return;
560567
}
561-
_entries['.packages'] = new DevFSStringContent(sb.toString());
568+
_entries[fs.path.toUri('.packages')] = new DevFSStringContent(sb.toString());
562569
}
563570
}
564571
}
572+
/// Converts a platform-specific file path to a platform-independent Uri path.
573+
String _asUriPath(String filePath) => fs.path.toUri(filePath).path + '/';

packages/flutter_tools/lib/src/run_hot.dart

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -434,17 +434,15 @@ class HotRunner extends ResidentRunner {
434434
String reloadMessage;
435435
try {
436436
String entryPath = fs.path.relative(mainPath, from: projectRootPath);
437-
String deviceEntryPath =
438-
_devFS.baseUri.resolve(entryPath).toFilePath();
439-
String devicePackagesPath =
440-
_devFS.baseUri.resolve('.packages').toFilePath();
437+
Uri deviceEntryUri = _devFS.baseUri.resolveUri(fs.path.toUri(entryPath));
438+
Uri devicePackagesUri = _devFS.baseUri.resolve('.packages');
441439
if (benchmarkMode)
442440
vmReloadTimer.start();
443441
Map<String, dynamic> reloadReport =
444442
await currentView.uiIsolate.reloadSources(
445443
pause: pause,
446-
rootLibPath: deviceEntryPath,
447-
packagesPath: devicePackagesPath);
444+
rootLibUri: deviceEntryUri,
445+
packagesUri: devicePackagesUri);
448446
if (!validateReloadReport(reloadReport)) {
449447
// Reload failed.
450448
flutterUsage.sendEvent('hot', 'reload-reject');

packages/flutter_tools/lib/src/vmservice.dart

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -838,17 +838,19 @@ class Isolate extends ServiceObjectOwner {
838838

839839
Future<Map<String, dynamic>> reloadSources(
840840
{ bool pause: false,
841-
String rootLibPath,
842-
String packagesPath}) async {
841+
Uri rootLibUri,
842+
Uri packagesUri}) async {
843843
try {
844844
Map<String, dynamic> arguments = <String, dynamic>{
845845
'pause': pause
846846
};
847-
if (rootLibPath != null) {
848-
arguments['rootLibUri'] = rootLibPath;
847+
// TODO(goderbauer): Transfer Uri (instead of file path) when remote end supports it.
848+
// Note: Despite the name, `rootLibUri` and `packagesUri` expect file paths.
849+
if (rootLibUri != null) {
850+
arguments['rootLibUri'] = rootLibUri.toFilePath(windows: false);
849851
}
850-
if (packagesPath != null) {
851-
arguments['packagesUri'] = packagesPath;
852+
if (packagesUri != null) {
853+
arguments['packagesUri'] = packagesUri.toFilePath(windows: false);
852854
}
853855
Map<String, dynamic> response = await invokeRpcRaw('_reloadSources', params: arguments);
854856
return response;

0 commit comments

Comments
 (0)