Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(438)

Unified Diff: runtime/bin/vmservice/loader.dart

Issue 1998963003: Rework standalone to use a synchronous loader that does not invoke Dart code (Closed) Base URL: git@github.com:dart-lang/sdk.git@master
Patch Set: Created 4 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « runtime/bin/main.cc ('k') | runtime/bin/vmservice/vmservice_io.dart » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: runtime/bin/vmservice/loader.dart
diff --git a/runtime/bin/vmservice/loader.dart b/runtime/bin/vmservice/loader.dart
index 7bec4a1cda1c23147d5e19bd897ff7deeea1a677..42bc6f92c948ef4bfcb5b8ad151b30c284c7afce 100644
--- a/runtime/bin/vmservice/loader.dart
+++ b/runtime/bin/vmservice/loader.dart
@@ -4,6 +4,303 @@
part of vmservice_io;
+_sanitizeWindowsPath(path) {
+ // For Windows we need to massage the paths a bit according to
+ // http://blogs.msdn.com/b/ie/archive/2006/12/06/file-uris-in-windows.aspx
+ //
+ // Convert
+ // C:\one\two\three
+ // to
+ // /C:/one/two/three
+
+ if (_isWindows == false) {
+ // Do nothing when not running Windows.
+ return path;
+ }
+
+ var fixedPath = "${path.replaceAll('\\', '/')}";
+
+ if ((path.length > 2) && (path[1] == ':')) {
+ // Path begins with a drive letter.
+ return '/$fixedPath';
+ }
+
+ return fixedPath;
+}
+
+_trimWindowsPath(path) {
+ // Convert /X:/ to X:/.
+ if (_isWindows == false) {
+ // Do nothing when not running Windows.
+ return path;
+ }
+ if (!path.startsWith('/') || (path.length < 3)) {
+ return path;
+ }
+ // Match '/?:'.
+ if ((path[0] == '/') && (path[2] == ':')) {
+ // Remove leading '/'.
+ return path.substring(1);
+ }
+ return path;
+}
+
+// Ensure we have a trailing slash character.
+_enforceTrailingSlash(uri) {
+ if (!uri.endsWith('/')) {
+ return '$uri/';
+ }
+ return uri;
+}
+
+// State associated with the isolate that is used for loading.
+class IsolateLoaderState extends IsolateEmbedderData {
+ IsolateLoaderState(this.isolateId);
+
+ final int isolateId;
+
+ SendPort sp;
+
+ void init(String packageRootFlag,
+ String packagesConfigFlag,
+ String workingDirectory,
+ String rootScript) {
+ // _workingDirectory must be set first.
+ _workingDirectory = new Uri.directory(workingDirectory);
+ if (rootScript != null) {
+ _rootScript = Uri.parse(rootScript);
+ }
+ // If the --package-root flag was passed.
+ if (packageRootFlag != null) {
+ _setPackageRoot(packageRootFlag);
+ }
+ // If the --packages flag was passed.
+ if (packagesConfigFlag != null) {
+ _setPackagesConfig(packagesConfigFlag);
+ }
+ }
+
+ void cleanup() {
+ if (_packagesPort != null) {
+ _packagesPort.close();
+ _packagesPort = null;
+ }
+ }
+
+ // The working directory when the embedder started.
+ Uri _workingDirectory;
+
+ // The root script's uri.
+ Uri _rootScript;
+
+ bool _traceLoading = false;
+
+ // Packages are either resolved looking up in a map or resolved from within a
+ // package root.
+ bool get _packagesReady => (_packageRoot != null) ||
+ (_packageMap != null) ||
+ (_packageError != null);
+
+ // Error string set if there was an error resolving package configuration.
+ // For example not finding a .packages file or packages/ directory, malformed
+ // .packages file or any other related error.
+ String _packageError = null;
+
+ // The directory to look in to resolve "package:" scheme URIs. By default it
+ // is the 'packages' directory right next to the script.
+ Uri _packageRoot = null;
+
+ // The map describing how certain package names are mapped to Uris.
+ Uri _packageConfig = null;
+ Map<String, Uri> _packageMap = null;
+
+ _setPackageRoot(String packageRoot) {
+ packageRoot = _enforceTrailingSlash(packageRoot);
+ if (packageRoot.startsWith('file:') ||
+ packageRoot.startsWith('http:') ||
+ packageRoot.startsWith('https:')) {
+ _packageRoot = _workingDirectory.resolve(packageRoot);
+ } else {
+ packageRoot = _sanitizeWindowsPath(packageRoot);
+ packageRoot = _trimWindowsPath(packageRoot);
+ _packageRoot = _workingDirectory.resolveUri(new Uri.file(packageRoot));
+ }
+ }
+
+ _setPackagesConfig(String packagesParam) {
+ var packagesName = _sanitizeWindowsPath(packagesParam);
+ var packagesUri = Uri.parse(packagesName);
+ if (packagesUri.scheme == '') {
+ // Script does not have a scheme, assume that it is a path,
+ // resolve it against the working directory.
+ packagesUri = _workingDirectory.resolveUri(packagesUri);
+ }
+ _requestPackagesMap(packagesUri);
+ _pendingPackageLoads.add(() {
+ // Dummy action.
+ });
+ }
+
+ // Handling of access to the package root or package map from user code.
+ _triggerPackageResolution(action) {
+ if (_packagesReady) {
+ // Packages are ready. Execute the action now.
+ action();
+ } else {
+ if (_pendingPackageLoads.isEmpty) {
+ // Package resolution has not been setup yet, and this is the first
+ // request for package resolution & loading.
+ _requestPackagesMap();
+ }
+ // Register the action for when the package resolution is ready.
+ _pendingPackageLoads.add(action);
+ }
+ }
+
+ // A list of callbacks which should be invoked after the package map has been
+ // loaded.
+ List<Function> _pendingPackageLoads = [];
+
+ // Given a uri with a 'package' scheme, return a Uri that is prefixed with
+ // the package root.
+ Uri _resolvePackageUri(Uri uri) {
+ assert(uri.scheme == "package");
+ assert(_packagesReady);
+
+ if (!uri.host.isEmpty) {
+ var path = '${uri.host}${uri.path}';
+ var right = 'package:$path';
+ var wrong = 'package://$path';
+
+ throw "URIs using the 'package:' scheme should look like "
+ "'$right', not '$wrong'.";
+ }
+
+ if (_traceLoading) {
+ _log('Resolving package with uri path: ${uri.path}');
+ }
+ var resolvedUri;
+ if (_packageError != null) {
+ if (_traceLoading) {
+ _log("Resolving package with pending resolution error: $_packageError");
+ }
+ throw _packageError;
+ } else if (_packageRoot != null) {
+ resolvedUri = _packageRoot.resolve(uri.path);
+ } else {
+ var packageName = uri.pathSegments[0];
+ var mapping = _packageMap[packageName];
+ if (_traceLoading) {
+ _log("Mapped '$packageName' package to '$mapping'");
+ }
+ if (mapping == null) {
+ throw "No mapping for '$packageName' package when resolving '$uri'.";
+ }
+ var path;
+ if (uri.path.length > packageName.length) {
+ path = uri.path.substring(packageName.length + 1);
+ } else {
+ // Handle naked package resolution to the default package name:
+ // package:foo is equivalent to package:foo/foo.dart
+ assert(uri.path.length == packageName.length);
+ path = "$packageName.dart";
+ }
+ if (_traceLoading) {
+ _log("Path to be resolved in package: $path");
+ }
+ resolvedUri = mapping.resolve(path);
+ }
+ if (_traceLoading) {
+ _log("Resolved '$uri' to '$resolvedUri'.");
+ }
+ return resolvedUri;
+ }
+
+ RawReceivePort _packagesPort;
+
+ void _requestPackagesMap([Uri packageConfig]) {
+ assert(_packagesPort == null);
+ assert(_rootScript != null);
+ // Create a port to receive the packages map on.
+ _packagesPort = new RawReceivePort(_handlePackagesReply);
+ var sp = _packagesPort.sendPort;
+
+ if (packageConfig != null) {
+ // Explicitly specified .packages path.
+ _handlePackagesRequest(sp,
+ _traceLoading,
+ -2,
+ packageConfig);
+ } else {
+ // Search for .packages or packages/ starting at the root script.
+ _handlePackagesRequest(sp,
+ _traceLoading,
+ -1,
+ _rootScript);
+ }
+
+ if (_traceLoading) {
+ _log("Requested packages map for '$_rootScript'.");
+ }
+ }
+
+ void _handlePackagesReply(msg) {
+ assert(_packagesPort != null);
+ // Make sure to close the _packagePort before any other action.
+ _packagesPort.close();
+ _packagesPort = null;
+
+ if (_traceLoading) {
+ _log("Got packages reply: $msg");
+ }
+ if (msg is String) {
+ if (_traceLoading) {
+ _log("Got failure response on package port: '$msg'");
+ }
+ // Remember the error message.
+ _packageError = msg;
+ } else if (msg is List) {
+ if (msg.length == 1) {
+ if (_traceLoading) {
+ _log("Received package root: '${msg[0]}'");
+ }
+ _packageRoot = Uri.parse(msg[0]);
+ } else {
+ // First entry contains the location of the loaded .packages file.
+ assert((msg.length % 2) == 0);
+ assert(msg.length >= 2);
+ assert(msg[1] == null);
+ _packageConfig = Uri.parse(msg[0]);
+ _packageMap = new Map<String, Uri>();
+ for (var i = 2; i < msg.length; i+=2) {
+ // TODO(iposva): Complain about duplicate entries.
+ _packageMap[msg[i]] = Uri.parse(msg[i+1]);
+ }
+ if (_traceLoading) {
+ _log("Setup package map: $_packageMap");
+ }
+ }
+ } else {
+ _packageError = "Bad type of packages reply: ${msg.runtimeType}";
+ if (_traceLoading) {
+ _log(_packageError);
+ }
+ }
+
+ // Resolve all pending package loads now that we know how to resolve them.
+ while (_pendingPackageLoads.length > 0) {
+ // Order does not matter as we queue all of the requests up right now.
+ var req = _pendingPackageLoads.removeLast();
+ // Call the registered closure, to handle the delayed action.
+ req();
+ }
+ // Reset the pending package loads to empty. So that we eventually can
+ // finish loading.
+ _pendingPackageLoads = [];
+ }
+
+}
+
_log(msg) {
print("% $msg");
}
@@ -11,19 +308,38 @@ _log(msg) {
var _httpClient;
// Send a response to the requesting isolate.
-void _sendResourceResponse(SendPort sp, int id, dynamic data) {
+void _sendResourceResponse(SendPort sp,
+ int tag,
+ Uri uri,
+ String libraryUrl,
+ dynamic data) {
assert((data is List<int>) || (data is String));
- var msg = new List(2);
- msg[0] = id;
- msg[1] = data;
+ var msg = new List(4);
+ if (data is String) {
+ // We encountered an error, flip the sign of the tag to indicate that.
+ tag = -tag;
+ if (libraryUrl == null) {
+ data = 'Could not load "$uri": $data';
+ } else {
+ data = 'Could not import "$uri" from "$libraryUrl": $data';
+ }
+ }
+ msg[0] = tag;
+ msg[1] = uri.toString();
+ msg[2] = libraryUrl;
+ msg[3] = data;
sp.send(msg);
}
-void _loadHttp(SendPort sp, int id, Uri uri) {
+void _loadHttp(SendPort sp,
+ int tag,
+ Uri uri,
+ Uri resolvedUri,
+ String libraryUrl) {
if (_httpClient == null) {
_httpClient = new HttpClient()..maxConnectionsPerHost = 6;
}
- _httpClient.getUrl(uri)
+ _httpClient.getUrl(resolvedUri)
.then((HttpClientRequest request) => request.close())
.then((HttpClientResponse response) {
var builder = new BytesBuilder(copy: false);
@@ -31,37 +347,46 @@ void _loadHttp(SendPort sp, int id, Uri uri) {
builder.add,
onDone: () {
if (response.statusCode != 200) {
- var msg = "Failure getting $uri:\n"
+ var msg = "Failure getting $resolvedUri:\n"
" ${response.statusCode} ${response.reasonPhrase}";
- _sendResourceResponse(sp, id, msg);
+ _sendResourceResponse(sp, tag, uri, libraryUrl, msg);
} else {
- _sendResourceResponse(sp, id, builder.takeBytes());
+ _sendResourceResponse(sp, tag, uri, libraryUrl,
+ builder.takeBytes());
}
},
onError: (e) {
- _sendResourceResponse(sp, id, e.toString());
+ _sendResourceResponse(sp, tag, uri, libraryUrl, e.toString());
});
})
.catchError((e) {
- _sendResourceResponse(sp, id, e.toString());
+ _sendResourceResponse(sp, tag, uri, libraryUrl, e.toString());
});
// It's just here to push an event on the event loop so that we invoke the
// scheduled microtasks.
Timer.run(() {});
}
-void _loadFile(SendPort sp, int id, Uri uri) {
- var path = uri.toFilePath();
+void _loadFile(SendPort sp,
+ int tag,
+ Uri uri,
+ Uri resolvedUri,
+ String libraryUrl) {
+ var path = resolvedUri.toFilePath();
var sourceFile = new File(path);
sourceFile.readAsBytes().then((data) {
- _sendResourceResponse(sp, id, data);
+ _sendResourceResponse(sp, tag, uri, libraryUrl, data);
},
onError: (e) {
- _sendResourceResponse(sp, id, e.toString());
+ _sendResourceResponse(sp, tag, uri, libraryUrl, e.toString());
});
}
-void _loadDataUri(SendPort sp, int id, Uri uri) {
+void _loadDataUri(SendPort sp,
+ int tag,
+ Uri uri,
+ Uri resolvedUri,
+ String libraryUrl) {
try {
var mime = uri.data.mimeType;
if ((mime != "application/dart") &&
@@ -74,25 +399,102 @@ void _loadDataUri(SendPort sp, int id, Uri uri) {
// The C++ portion of the embedder assumes UTF-8.
throw "Only utf-8 or US-ASCII encodings are supported: $charset given.";
}
- _sendResourceResponse(sp, id, uri.data.contentAsBytes());
+ _sendResourceResponse(sp, tag, uri, libraryUrl, uri.data.contentAsBytes());
} catch (e) {
- _sendResourceResponse(sp, id, "Invalid data uri ($uri):\n $e");
+ _sendResourceResponse(sp, tag, uri, libraryUrl,
+ "Invalid data uri ($uri):\n $e");
}
}
-_handleResourceRequest(SendPort sp, bool traceLoading, int id, Uri resource) {
- if (resource.scheme == 'file') {
- _loadFile(sp, id, resource);
- } else if ((resource.scheme == 'http') || (resource.scheme == 'https')) {
- _loadHttp(sp, id, resource);
- } else if ((resource.scheme == 'data')) {
- _loadDataUri(sp, id, resource);
+// Loading a package URI needs to first map the package name to a loadable
+// URI.
+_loadPackage(IsolateLoaderState loaderState,
+ SendPort sp,
+ bool traceLoading,
+ int tag,
+ Uri uri,
+ Uri resolvedUri,
+ String libraryUrl) {
+ if (loaderState._packagesReady) {
+ var resolvedUri;
+ try {
+ resolvedUri = loaderState._resolvePackageUri(uri);
+ } catch (e, s) {
+ if (traceLoading) {
+ _log("Exception ($e) when resolving package URI: $uri");
+ }
+ // Report error.
+ _sendResourceResponse(sp,
+ tag,
+ uri,
+ libraryUrl,
+ e.toString());
+ return;
+ }
+ // Recursively call with the new resolved uri.
+ _handleResourceRequest(loaderState,
+ sp,
+ traceLoading,
+ tag,
+ uri,
+ resolvedUri,
+ libraryUrl);
} else {
- _sendResourceResponse(sp, id,
- 'Unknown scheme (${resource.scheme}) for $resource');
+ if (loaderState._pendingPackageLoads.isEmpty) {
+ // Package resolution has not been setup yet, and this is the first
+ // request for package resolution & loading.
+ loaderState._requestPackagesMap();
+ }
+ // Register the action of loading this package once the package resolution
+ // is ready.
+ loaderState._pendingPackageLoads.add(() {
+ _handleResourceRequest(loaderState,
+ sp,
+ traceLoading,
+ tag,
+ uri,
+ uri,
+ libraryUrl);
+ });
+ if (traceLoading) {
+ _log("Pending package load of '$uri': "
+ "${loaderState._pendingPackageLoads.length} pending");
+ }
}
}
+// TODO(johnmccutchan): This and most other top level functions in this file
+// should be turned into methods on the IsolateLoaderState class.
+_handleResourceRequest(IsolateLoaderState loaderState,
+ SendPort sp,
+ bool traceLoading,
+ int tag,
+ Uri uri,
+ Uri resolvedUri,
+ String libraryUrl) {
+ if (resolvedUri.scheme == 'file') {
+ _loadFile(sp, tag, uri, resolvedUri, libraryUrl);
+ } else if ((resolvedUri.scheme == 'http') ||
+ (resolvedUri.scheme == 'https')) {
+ _loadHttp(sp, tag, uri, resolvedUri, libraryUrl);
+ } else if ((resolvedUri.scheme == 'data')) {
+ _loadDataUri(sp, tag, uri, resolvedUri, libraryUrl);
+ } else if ((resolvedUri.scheme == 'package')) {
+ _loadPackage(loaderState,
+ sp,
+ traceLoading,
+ tag,
+ uri,
+ resolvedUri,
+ libraryUrl);
+ } else {
+ _sendResourceResponse(sp, tag,
+ uri,
+ libraryUrl,
+ 'Unknown scheme (${resolvedUri.scheme}) for '
+ '$resolvedUri');
+ }
+}
// Handling of packages requests. Finding and parsing of .packages file or
// packages/ directories.
@@ -329,7 +731,6 @@ _findPackagesFile(SendPort sp, bool traceLoading, Uri base) async {
}
}
-
Future<bool> _loadHttpPackagesFile(SendPort sp,
bool traceLoading,
Uri resource) async {
@@ -386,13 +787,16 @@ _loadPackagesData(sp, traceLoading, resource){
}
}
-
+// This code used to exist in a second isolate and so it uses a SendPort to
+// report it's return value. This could be refactored so that it returns it's
+// value and the caller could wait on the future rather than a message on
+// SendPort.
_handlePackagesRequest(SendPort sp,
bool traceLoading,
- int id,
+ int tag,
Uri resource) async {
try {
- if (id == -1) {
+ if (tag == -1) {
if (resource.scheme == 'file') {
_findPackagesFile(sp, traceLoading, resource);
} else if ((resource.scheme == 'http') || (resource.scheme == 'https')) {
@@ -409,7 +813,7 @@ _handlePackagesRequest(SendPort sp,
sp.send("Unsupported scheme used to locate .packages file: "
"'$resource'.");
}
- } else if (id == -2) {
+ } else if (tag == -2) {
if (traceLoading) {
_log("Handling load of packages map: '$resource'.");
}
@@ -432,7 +836,7 @@ _handlePackagesRequest(SendPort sp,
"'$resource'.");
}
} else {
- sp.send("Unknown packages request id: $id for '$resource'.");
+ sp.send("Unknown packages request tag: $tag for '$resource'.");
}
} catch (e, s) {
if (traceLoading) {
@@ -442,21 +846,143 @@ _handlePackagesRequest(SendPort sp,
}
}
+// Shutdown all active loaders by sending an error message.
+void shutdownLoaders() {
+ String message = 'Service shutdown';
+ if (_httpClient != null) {
+ _httpClient.close(force: true);
+ _httpClient = null;
+ }
+ isolateEmbedderData.values.toList().forEach((IsolateLoaderState ils) {
+ ils.cleanup();
+ assert(ils.sp != null);
+ _sendResourceResponse(ils.sp, 1, null, null, message);
+ });
+}
+
+// See Dart_LibraryTag in dart_api.h
+const _Dart_kCanonicalizeUrl = 0; // Canonicalize the URL.
+const _Dart_kScriptTag = 1; // Load the root script.
+const _Dart_kSourceTag = 2; // Load a part source.
+const _Dart_kImportTag = 3; // Import a library.
+
+// Extra requests. Keep these in sync between loader.dart and builtin.dart.
+const _Dart_kInitLoader = 4; // Initialize the loader.
+const _Dart_kResourceLoad = 5; // Resource class support.
+const _Dart_kGetPackageRootUri = 6; // Uri of the packages/ directory.
+const _Dart_kGetPackageConfigUri = 7; // Uri of the .packages file.
+const _Dart_kResolvePackageUri = 8; // Resolve a package: uri.
// External entry point for loader requests.
_processLoadRequest(request) {
- SendPort sp = request[0];
- assert(sp != null);
- bool traceLoading = request[1];
- assert(traceLoading != null);
- int id = request[2];
- assert(id != null);
- String resource = request[3];
- assert(resource != null);
- var uri = Uri.parse(resource);
- if (id >= 0) {
- _handleResourceRequest(sp, traceLoading, id, uri);
- } else {
- _handlePackagesRequest(sp, traceLoading, id, uri);
+ assert(request is List);
+ assert(request.length > 4);
+
+ // Should we trace loading?
+ bool traceLoading = request[0];
+
+ // This is the sending isolate's Dart_GetMainPortId().
+ int isolateId = request[1];
+
+ // The tag describing the operation.
+ int tag = request[2];
+
+ // The send port to send the response on.
+ SendPort sp = request[3];
+
+ // Grab the loader state for the requesting isolate.
+ IsolateLoaderState loaderState = isolateEmbedderData[isolateId];
+
+ // We are either about to initialize the loader, or, we already have.
+ assert((tag == _Dart_kInitLoader) || (loaderState != null));
+
+ // Handle the request specified in the tag.
+ switch (tag) {
+ case _Dart_kScriptTag: {
+ Uri uri = Uri.parse(request[4]);
+ // Remember the root script.
+ loaderState._rootScript = uri;
+ _handleResourceRequest(loaderState,
+ sp,
+ traceLoading,
+ tag,
+ uri,
+ uri,
+ null);
+ }
+ break;
+ case _Dart_kSourceTag:
+ case _Dart_kImportTag: {
+ // The url of the file being loaded.
+ var uri = Uri.parse(request[4]);
+ // The library that is importing/parting the file.
+ String libraryUrl = request[5];
+ _handleResourceRequest(loaderState,
+ sp,
+ traceLoading,
+ tag,
+ uri,
+ uri,
+ libraryUrl);
+ }
+ break;
+ case _Dart_kInitLoader: {
+ String packageRoot = request[4];
+ String packagesFile = request[5];
+ String workingDirectory = request[6];
+ String rootScript = request[7];
+ if (loaderState == null) {
+ loaderState = new IsolateLoaderState(isolateId);
+ isolateEmbedderData[isolateId] = loaderState;
+ loaderState.init(packageRoot,
+ packagesFile,
+ workingDirectory,
+ rootScript);
+ }
+ loaderState.sp = sp;
+ assert(isolateEmbedderData[isolateId] == loaderState);
+ }
+ break;
+ case _Dart_kResourceLoad: {
+ Uri uri = Uri.parse(request[4]);
+ _handleResourceRequest(loaderState,
+ sp,
+ traceLoading,
+ tag,
+ uri,
+ uri,
+ null);
+ }
+ break;
+ case _Dart_kGetPackageRootUri:
+ loaderState._triggerPackageResolution(() {
+ // Respond with the package root (if any) after package resolution.
+ sp.send(loaderState._packageRoot);
+ });
+ break;
+ case _Dart_kGetPackageConfigUri:
+ loaderState._triggerPackageResolution(() {
+ // Respond with the packages config (if any) after package resolution.
+ sp.send(loaderState._packageConfig);
+ });
+ break;
+ case _Dart_kResolvePackageUri:
+ Uri uri = Uri.parse(request[4]);
+ loaderState._triggerPackageResolution(() {
+ // Respond with the resolved package uri after package resolution.
+ Uri resolvedUri;
+ try {
+ resolvedUri = loaderState._resolvePackageUri(uri);
+ } catch (e, s) {
+ if (traceLoading) {
+ _log("Exception ($e) when resolving package URI: $uri");
+ }
+ resolvedUri = null;
+ }
+ sp.send(resolvedUri);
+ });
+ break;
+ default:
+ _log('Unknown loader request tag=$tag from $isolateId');
}
}
« no previous file with comments | « runtime/bin/main.cc ('k') | runtime/bin/vmservice/vmservice_io.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698