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

Side by Side Diff: sdk/lib/vmservice/vmservice.dart

Issue 2980733003: Introduced support for external services registration in the ServiceProtocol (Closed)
Patch Set: Address comments Created 3 years, 5 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 unified diff | Download patch
« no previous file with comments | « sdk/lib/vmservice/running_isolates.dart ('k') | sdk/lib/vmservice/vmservice_sources.gypi » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a 2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file. 3 // BSD-style license that can be found in the LICENSE file.
4 4
5 library dart._vmservice; 5 library dart._vmservice;
6 6
7 import 'dart:async'; 7 import 'dart:async';
8 import 'dart:collection'; 8 import 'dart:collection';
9 import 'dart:convert'; 9 import 'dart:convert';
10 import 'dart:developer' show ServiceProtocolInfo; 10 import 'dart:developer' show ServiceProtocolInfo;
11 import 'dart:isolate'; 11 import 'dart:isolate';
12 import 'dart:math'; 12 import 'dart:math';
13 import 'dart:typed_data'; 13 import 'dart:typed_data';
14 14
15 part 'asset.dart'; 15 part 'asset.dart';
16 part 'client.dart'; 16 part 'client.dart';
17 part 'devfs.dart'; 17 part 'devfs.dart';
18 part 'constants.dart'; 18 part 'constants.dart';
19 part 'running_isolate.dart'; 19 part 'running_isolate.dart';
20 part 'running_isolates.dart'; 20 part 'running_isolates.dart';
21 part 'message.dart'; 21 part 'message.dart';
22 part 'message_router.dart'; 22 part 'message_router.dart';
23 part 'named_lookup.dart';
23 24
24 final RawReceivePort isolateControlPort = new RawReceivePort(); 25 final RawReceivePort isolateControlPort = new RawReceivePort();
25 final RawReceivePort scriptLoadPort = new RawReceivePort(); 26 final RawReceivePort scriptLoadPort = new RawReceivePort();
26 27
27 abstract class IsolateEmbedderData { 28 abstract class IsolateEmbedderData {
28 void cleanup(); 29 void cleanup();
29 } 30 }
30 31
31 String _makeAuthToken() { 32 String _makeAuthToken() {
32 final kTokenByteSize = 8; 33 final kTokenByteSize = 8;
(...skipping 11 matching lines...) Expand all
44 // TODO(johnmccutchan): Enable the auth token and drop the origin check. 45 // TODO(johnmccutchan): Enable the auth token and drop the origin check.
45 final bool useAuthToken = const bool.fromEnvironment('DART_SERVICE_USE_AUTH'); 46 final bool useAuthToken = const bool.fromEnvironment('DART_SERVICE_USE_AUTH');
46 47
47 // This is for use by the embedder. It is a map from the isolateId to 48 // This is for use by the embedder. It is a map from the isolateId to
48 // anything implementing IsolateEmbedderData. When an isolate goes away, 49 // anything implementing IsolateEmbedderData. When an isolate goes away,
49 // the cleanup method will be invoked after being removed from the map. 50 // the cleanup method will be invoked after being removed from the map.
50 final Map<int, IsolateEmbedderData> isolateEmbedderData = 51 final Map<int, IsolateEmbedderData> isolateEmbedderData =
51 new Map<int, IsolateEmbedderData>(); 52 new Map<int, IsolateEmbedderData>();
52 53
53 // These must be kept in sync with the declarations in vm/json_stream.h. 54 // These must be kept in sync with the declarations in vm/json_stream.h.
55 const kParseError = -32700;
56 const kInvalidRequest = -32600;
57 const kMethodNotFound = -32601;
54 const kInvalidParams = -32602; 58 const kInvalidParams = -32602;
55 const kInternalError = -32603; 59 const kInternalError = -32603;
60
61 const kExtensionError = -32000;
62
56 const kFeatureDisabled = 100; 63 const kFeatureDisabled = 100;
64 const kCannotAddBreakpoint = 102;
57 const kStreamAlreadySubscribed = 103; 65 const kStreamAlreadySubscribed = 103;
58 const kStreamNotSubscribed = 104; 66 const kStreamNotSubscribed = 104;
67 const kIsolateMustBeRunnable = 105;
68 const kIsolateMustBePaused = 106;
69 const kCannotResume = 107;
70 const kIsolateIsReloading = 108;
71 const kIsolateReloadBarred = 109;
72 const kServiceAlreadyRegistered = 110;
73 const kServiceDisappeared = 111;
74
75 // Experimental (used in private rpcs).
59 const kFileSystemAlreadyExists = 1001; 76 const kFileSystemAlreadyExists = 1001;
60 const kFileSystemDoesNotExist = 1002; 77 const kFileSystemDoesNotExist = 1002;
61 const kFileDoesNotExist = 1003; 78 const kFileDoesNotExist = 1003;
62 79
63 var _errorMessages = { 80 var _errorMessages = {
64 kInvalidParams: 'Invalid params', 81 kInvalidParams: 'Invalid params',
65 kInternalError: 'Internal error', 82 kInternalError: 'Internal error',
66 kFeatureDisabled: 'Feature is disabled', 83 kFeatureDisabled: 'Feature is disabled',
67 kStreamAlreadySubscribed: 'Stream already subscribed', 84 kStreamAlreadySubscribed: 'Stream already subscribed',
68 kStreamNotSubscribed: 'Stream not subscribed', 85 kStreamNotSubscribed: 'Stream not subscribed',
69 kFileSystemAlreadyExists: 'File system already exists', 86 kFileSystemAlreadyExists: 'File system already exists',
70 kFileSystemDoesNotExist: 'File system does not exist', 87 kFileSystemDoesNotExist: 'File system does not exist',
71 kFileDoesNotExist: 'File does not exist', 88 kFileDoesNotExist: 'File does not exist',
89 kServiceAlreadyRegistered: 'Service already registered',
90 kServiceDisappeared: 'Service has disappeared',
72 }; 91 };
73 92
74 String encodeRpcError(Message message, int code, {String details}) { 93 String encodeRpcError(Message message, int code, {String details}) {
75 var response = { 94 var response = {
76 'jsonrpc': '2.0', 95 'jsonrpc': '2.0',
77 'id': message.serial, 96 'id': message.serial,
78 'error': { 97 'error': {
79 'code': code, 98 'code': code,
80 'message': _errorMessages[code], 99 'message': _errorMessages[code],
81 }, 100 },
(...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after
135 /// Called to write a stream into a file. 154 /// Called to write a stream into a file.
136 typedef Future WriteStreamFileCallback(Uri path, Stream<List<int>> bytes); 155 typedef Future WriteStreamFileCallback(Uri path, Stream<List<int>> bytes);
137 156
138 /// Called to read a file. 157 /// Called to read a file.
139 typedef Future<List<int>> ReadFileCallback(Uri path); 158 typedef Future<List<int>> ReadFileCallback(Uri path);
140 159
141 /// Called to list all files under some path. 160 /// Called to list all files under some path.
142 typedef Future<List<Map<String, String>>> ListFilesCallback(Uri path); 161 typedef Future<List<Map<String, String>>> ListFilesCallback(Uri path);
143 162
144 /// Called when we need information about the server. 163 /// Called when we need information about the server.
164 typedef Future<Uri> ServerInformamessage_routertionCallback();
165
166 /// Called when we need information about the server.
145 typedef Future<Uri> ServerInformationCallback(); 167 typedef Future<Uri> ServerInformationCallback();
146 168
147 /// Called when we want to [enable] or disable the web server. 169 /// Called when we want to [enable] or disable the web server.
148 typedef Future<Uri> WebServerControlCallback(bool enable); 170 typedef Future<Uri> WebServerControlCallback(bool enable);
149 171
150 /// Hooks that are setup by the embedder. 172 /// Hooks that are setup by the embedder.
151 class VMServiceEmbedderHooks { 173 class VMServiceEmbedderHooks {
152 static ServerStartCallback serverStart; 174 static ServerStartCallback serverStart;
153 static ServerStopCallback serverStop; 175 static ServerStopCallback serverStop;
154 static CleanupCallback cleanup; 176 static CleanupCallback cleanup;
155 static CreateTempDirCallback createTempDir; 177 static CreateTempDirCallback createTempDir;
156 static DeleteDirCallback deleteDir; 178 static DeleteDirCallback deleteDir;
157 static WriteFileCallback writeFile; 179 static WriteFileCallback writeFile;
158 static WriteStreamFileCallback writeStreamFile; 180 static WriteStreamFileCallback writeStreamFile;
159 static ReadFileCallback readFile; 181 static ReadFileCallback readFile;
160 static ListFilesCallback listFiles; 182 static ListFilesCallback listFiles;
161 static ServerInformationCallback serverInformation; 183 static ServerInformationCallback serverInformation;
162 static WebServerControlCallback webServerControl; 184 static WebServerControlCallback webServerControl;
163 } 185 }
164 186
165 class VMService extends MessageRouter { 187 class VMService extends MessageRouter {
166 static VMService _instance; 188 static VMService _instance;
167 189
190 static const serviceNamespace = 's';
191
168 /// Collection of currently connected clients. 192 /// Collection of currently connected clients.
169 final Set<Client> clients = new Set<Client>(); 193 final NamedLookup<Client> clients =
194 new NamedLookup<Client>(prologue: serviceNamespace);
195 final IdGenerator _serviceRequests = new IdGenerator(prologue: 'sr');
170 196
171 /// Collection of currently running isolates. 197 /// Collection of currently running isolates.
172 RunningIsolates runningIsolates = new RunningIsolates(); 198 RunningIsolates runningIsolates = new RunningIsolates();
173 199
174 /// A port used to receive events from the VM. 200 /// A port used to receive events from the VM.
175 final RawReceivePort eventPort; 201 final RawReceivePort eventPort;
176 202
177 final devfs = new DevFS(); 203 final devfs = new DevFS();
178 204
179 void _addClient(Client client) { 205 void _addClient(Client client) {
180 assert(client.streams.isEmpty); 206 assert(client.streams.isEmpty);
207 assert(client.services.isEmpty);
181 clients.add(client); 208 clients.add(client);
182 } 209 }
183 210
184 void _removeClient(Client client) { 211 void _removeClient(Client client) {
212 final namespace = clients.keyOf(client);
185 clients.remove(client); 213 clients.remove(client);
186 for (var streamId in client.streams) { 214 for (var streamId in client.streams) {
187 if (!_isAnyClientSubscribed(streamId)) { 215 if (!_isAnyClientSubscribed(streamId)) {
188 _vmCancelStream(streamId); 216 _vmCancelStream(streamId);
189 } 217 }
190 } 218 }
219 for (var service in client.services.keys) {
220 _eventMessageHandler([
221 '_Service',
222 JSON.encode({
223 'jsonrpc': '2.0',
224 'method': 'streamNotify',
225 'params': {
226 'streamId': '_Service',
227 'event': {
228 "type": "Event",
229 "kind": "ServiceUnregistered",
230 'timestamp': new DateTime.now().millisecondsSinceEpoch,
231 'service': service,
232 'method': namespace + '.' + service,
233 }
234 }
235 })
236 ]);
237 }
238 // Complete all requestes as failed
239 for (var handle in client.serviceHandles.values) {
240 handle(null);
241 }
191 } 242 }
192 243
193 void _eventMessageHandler(List eventMessage) { 244 void _eventMessageHandler(List eventMessage) {
194 var streamId = eventMessage[0]; 245 var streamId = eventMessage[0];
195 var event = eventMessage[1]; 246 var event = eventMessage[1];
196 for (var client in clients) { 247 for (var client in clients) {
197 if (client.sendEvents && client.streams.contains(streamId)) { 248 if (client.sendEvents && client.streams.contains(streamId)) {
198 client.post(event); 249 client.post(event);
199 } 250 }
200 } 251 }
(...skipping 105 matching lines...) Expand 10 before | Expand all | Expand 10 after
306 357
307 bool _isAnyClientSubscribed(String streamId) { 358 bool _isAnyClientSubscribed(String streamId) {
308 for (var client in clients) { 359 for (var client in clients) {
309 if (client.streams.contains(streamId)) { 360 if (client.streams.contains(streamId)) {
310 return true; 361 return true;
311 } 362 }
312 } 363 }
313 return false; 364 return false;
314 } 365 }
315 366
367 static const kServiceStream = '_Service';
368 static const serviceStreams = const [kServiceStream];
369
316 Future<String> _streamListen(Message message) async { 370 Future<String> _streamListen(Message message) async {
317 var client = message.client; 371 var client = message.client;
318 var streamId = message.params['streamId']; 372 var streamId = message.params['streamId'];
319 373
320 if (client.streams.contains(streamId)) { 374 if (client.streams.contains(streamId)) {
321 return encodeRpcError(message, kStreamAlreadySubscribed); 375 return encodeRpcError(message, kStreamAlreadySubscribed);
322 } 376 }
323 if (!_isAnyClientSubscribed(streamId)) { 377 if (!_isAnyClientSubscribed(streamId)) {
324 if (!_vmListenStream(streamId)) { 378 if (!serviceStreams.contains(streamId) && !_vmListenStream(streamId)) {
325 return encodeRpcError(message, kInvalidParams, 379 return encodeRpcError(message, kInvalidParams,
326 details: "streamListen: invalid 'streamId' parameter: ${streamId}"); 380 details: "streamListen: invalid 'streamId' parameter: ${streamId}");
327 } 381 }
328 } 382 }
383
384 // Some streams can generate events or side effects after registration
385 switch (streamId) {
386 case kServiceStream:
387 for (Client c in clients) {
388 if (c == client) continue;
389 for (String service in c.services.keys) {
390 _sendServiceRegisteredEvent(c, service, target: client);
391 }
392 }
393 ;
394 break;
395 }
396
329 client.streams.add(streamId); 397 client.streams.add(streamId);
330
331 return encodeSuccess(message); 398 return encodeSuccess(message);
332 } 399 }
333 400
334 Future<String> _streamCancel(Message message) async { 401 Future<String> _streamCancel(Message message) async {
335 var client = message.client; 402 var client = message.client;
336 var streamId = message.params['streamId']; 403 var streamId = message.params['streamId'];
337 404
338 if (!client.streams.contains(streamId)) { 405 if (!client.streams.contains(streamId)) {
339 return encodeRpcError(message, kStreamNotSubscribed); 406 return encodeRpcError(message, kStreamNotSubscribed);
340 } 407 }
341 client.streams.remove(streamId); 408 client.streams.remove(streamId);
342 if (!_isAnyClientSubscribed(streamId)) { 409 if (!serviceStreams.contains(streamId) &&
410 !_isAnyClientSubscribed(streamId)) {
343 _vmCancelStream(streamId); 411 _vmCancelStream(streamId);
344 } 412 }
345 413
346 return encodeSuccess(message); 414 return encodeSuccess(message);
347 } 415 }
348 416
417 static bool _hasNamespace(String method) =>
418 method.contains('.') &&
419 _getNamespace(method).startsWith(serviceNamespace);
420 static String _getNamespace(String method) => method.split('.').first;
421 static String _getMethod(String method) => method.split('.').last;
422
423 Future<String> _registerService(Message message) async {
424 final client = message.client;
425 final service = message.params['service'];
426 final alias = message.params['alias'];
427
428 if (service is! String || service == '') {
429 return encodeRpcError(message, kInvalidParams,
430 details: "registerService: invalid 'service' parameter: ${service}");
431 }
432 if (alias is! String || alias == '') {
433 return encodeRpcError(message, kInvalidParams,
434 details: "registerService: invalid 'alias' parameter: ${alias}");
435 }
436 if (client.services.containsKey(service)) {
437 return encodeRpcError(message, kServiceAlreadyRegistered);
438 }
439 client.services[service] = alias;
440
441 bool removed;
442 try {
443 // Do not send streaming events to the client which registers the service
444 removed = client.streams.remove(kServiceStream);
445 await _sendServiceRegisteredEvent(client, service);
446 } finally {
447 if (removed) client.streams.add(kServiceStream);
448 }
449
450 return encodeSuccess(message);
451 }
452
453 _sendServiceRegisteredEvent(Client client, String service,
454 {Client target}) async {
455 final namespace = clients.keyOf(client);
456 final alias = client.services[service];
457 final event = JSON.encode({
458 'jsonrpc': '2.0',
459 'method': 'streamNotify',
460 'params': {
461 'streamId': kServiceStream,
462 'event': {
463 "type": "Event",
464 "kind": "ServiceRegistered",
465 'timestamp': new DateTime.now().millisecondsSinceEpoch,
466 'service': service,
467 'method': namespace + '.' + service,
468 'alias': alias
469 }
470 }
471 });
472 if (target == null) {
473 _eventMessageHandler([kServiceStream, event]);
474 } else {
475 target.post(event);
476 }
477 }
478
479 Future<String> _handleService(Message message) async {
480 final namespace = _getNamespace(message.method);
481 final method = _getMethod(message.method);
482 final client = clients[namespace];
483 if (client != null) {
484 if (client.services.containsKey(method)) {
485 final id = _serviceRequests.newId();
486 final oldId = message.serial;
487 final completer = new Completer<String>();
488 client.serviceHandles[id] = (Message m) {
489 if (m != null) {
490 completer.complete(JSON.encode(m.forwardToJson({'id': oldId})));
491 } else {
492 completer.complete(encodeRpcError(message, kServiceDisappeared));
493 }
494 };
495 client.post(
496 JSON.encode(message.forwardToJson({'id': id, 'method': method})));
497 return completer.future;
498 }
499 }
500 return encodeRpcError(message, kMethodNotFound,
501 details: "Unknown service: ${message.method}");
502 }
503
349 Future<String> _spawnUri(Message message) async { 504 Future<String> _spawnUri(Message message) async {
350 var token = message.params['token']; 505 var token = message.params['token'];
351 if (token == null) { 506 if (token == null) {
352 return encodeMissingParamError(message, 'token'); 507 return encodeMissingParamError(message, 'token');
353 } 508 }
354 if (token is! String) { 509 if (token is! String) {
355 return encodeInvalidParamError(message, 'token'); 510 return encodeInvalidParamError(message, 'token');
356 } 511 }
357 var uri = message.params['uri']; 512 var uri = message.params['uri'];
358 if (uri == null) { 513 if (uri == null) {
(...skipping 57 matching lines...) Expand 10 before | Expand all | Expand 10 after
416 var getFlagListResponse = responseAsJson( 571 var getFlagListResponse = responseAsJson(
417 await new Message.fromUri(client, getFlagList).sendToVM()); 572 await new Message.fromUri(client, getFlagList).sendToVM());
418 responses[getFlagList.toString()] = getFlagListResponse['result']; 573 responses[getFlagList.toString()] = getFlagListResponse['result'];
419 574
420 // Make requests to each isolate. 575 // Make requests to each isolate.
421 for (var isolate in isolates) { 576 for (var isolate in isolates) {
422 for (var request in perIsolateRequests) { 577 for (var request in perIsolateRequests) {
423 var message = new Message.forIsolate(client, request, isolate); 578 var message = new Message.forIsolate(client, request, isolate);
424 // Decode the JSON and and insert it into the map. The map key 579 // Decode the JSON and and insert it into the map. The map key
425 // is the request Uri. 580 // is the request Uri.
426 var response = responseAsJson(await isolate.route(message)); 581 var response = responseAsJson(await isolate.routeRequest(message));
427 responses[message.toUri().toString()] = response['result']; 582 responses[message.toUri().toString()] = response['result'];
428 } 583 }
429 // Dump the object id ring requests. 584 // Dump the object id ring requests.
430 var message = 585 var message =
431 new Message.forIsolate(client, Uri.parse('_dumpIdZone'), isolate); 586 new Message.forIsolate(client, Uri.parse('_dumpIdZone'), isolate);
432 var response = responseAsJson(await isolate.route(message)); 587 var response = responseAsJson(await isolate.routeRequest(message));
433 // Insert getObject requests into responses map. 588 // Insert getObject requests into responses map.
434 for (var object in response['result']['objects']) { 589 for (var object in response['result']['objects']) {
435 final requestUri = 590 final requestUri =
436 'getObject&isolateId=${isolate.serviceId}?objectId=${object["id"]}'; 591 'getObject&isolateId=${isolate.serviceId}?objectId=${object["id"]}';
437 responses[requestUri] = object; 592 responses[requestUri] = object;
438 } 593 }
439 } 594 }
440 595
441 // Encode the entire crash dump. 596 // Encode the entire crash dump.
442 return encodeResult(message, responses); 597 return encodeResult(message, responses);
443 } 598 }
444 599
445 Future<String> route(Message message) { 600 Future routeRequest(Message message) async {
446 if (message.completed) { 601 try {
602 if (message.completed) {
603 return await message.response;
604 }
605 // TODO(turnidge): Update to json rpc. BEFORE SUBMIT.
606 if (message.method == '_getCrashDump') {
607 return await _getCrashDump(message);
608 }
609 if (message.method == 'streamListen') {
610 return await _streamListen(message);
611 }
612 if (message.method == 'streamCancel') {
613 return await _streamCancel(message);
614 }
615 if (message.method == '_registerService') {
616 return await _registerService(message);
617 }
618 if (message.method == '_spawnUri') {
619 return await _spawnUri(message);
620 }
621 if (devfs.shouldHandleMessage(message)) {
622 return await devfs.handleMessage(message);
623 }
624 if (_hasNamespace(message.method)) {
625 return await _handleService(message);
626 }
627 if (message.params['isolateId'] != null) {
628 return await runningIsolates.routeRequest(message);
629 }
630 return await message.sendToVM();
631 } catch (e, st) {
632 message.setErrorResponse(kInternalError, 'Unexpected exception:$e\n$st');
447 return message.response; 633 return message.response;
448 } 634 }
449 // TODO(turnidge): Update to json rpc. BEFORE SUBMIT. 635 }
450 if (message.method == '_getCrashDump') { 636
451 return _getCrashDump(message); 637 void routeResponse(message) {
638 final client = message.client;
639 if (client.serviceHandles.containsKey(message.serial)) {
640 client.serviceHandles.remove(message.serial)(message);
641 _serviceRequests.release(message.serial);
452 } 642 }
453 if (message.method == 'streamListen') {
454 return _streamListen(message);
455 }
456 if (message.method == 'streamCancel') {
457 return _streamCancel(message);
458 }
459 if (message.method == '_spawnUri') {
460 return _spawnUri(message);
461 }
462 if (devfs.shouldHandleMessage(message)) {
463 return devfs.handleMessage(message);
464 }
465 if (message.params['isolateId'] != null) {
466 return runningIsolates.route(message);
467 }
468 return message.sendToVM();
469 } 643 }
470 } 644 }
471 645
472 RawReceivePort boot() { 646 RawReceivePort boot() {
473 // Return the port we expect isolate control messages on. 647 // Return the port we expect isolate control messages on.
474 return isolateControlPort; 648 return isolateControlPort;
475 } 649 }
476 650
477 void _registerIsolate(int port_id, SendPort sp, String name) { 651 void _registerIsolate(int port_id, SendPort sp, String name) {
478 var service = new VMService(); 652 var service = new VMService();
(...skipping 14 matching lines...) Expand all
493 bool _vmListenStream(String streamId) native "VMService_ListenStream"; 667 bool _vmListenStream(String streamId) native "VMService_ListenStream";
494 668
495 /// Cancel a subscription to a service stream. 669 /// Cancel a subscription to a service stream.
496 void _vmCancelStream(String streamId) native "VMService_CancelStream"; 670 void _vmCancelStream(String streamId) native "VMService_CancelStream";
497 671
498 /// Get the bytes to the tar archive. 672 /// Get the bytes to the tar archive.
499 Uint8List _requestAssets() native "VMService_RequestAssets"; 673 Uint8List _requestAssets() native "VMService_RequestAssets";
500 674
501 /// Notify the vm service that an isolate has been spawned via rpc. 675 /// Notify the vm service that an isolate has been spawned via rpc.
502 void _spawnUriNotify(obj, String token) native "VMService_spawnUriNotify"; 676 void _spawnUriNotify(obj, String token) native "VMService_spawnUriNotify";
OLDNEW
« no previous file with comments | « sdk/lib/vmservice/running_isolates.dart ('k') | sdk/lib/vmservice/vmservice_sources.gypi » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698