| OLD | NEW |
| 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2014, 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 service_common; | 5 library service_common; |
| 6 | 6 |
| 7 import 'dart:async'; | 7 import 'dart:async'; |
| 8 import 'dart:convert'; | 8 import 'dart:convert'; |
| 9 import 'dart:typed_data'; | 9 import 'dart:typed_data'; |
| 10 | 10 |
| (...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 49 'chrome': chrome, | 49 'chrome': chrome, |
| 50 'name': name, | 50 'name': name, |
| 51 'networkAddress': networkAddress, | 51 'networkAddress': networkAddress, |
| 52 }; | 52 }; |
| 53 } | 53 } |
| 54 } | 54 } |
| 55 | 55 |
| 56 class _WebSocketRequest { | 56 class _WebSocketRequest { |
| 57 final String method; | 57 final String method; |
| 58 final Map params; | 58 final Map params; |
| 59 final Completer<String> completer; | 59 final Completer<Map> completer; |
| 60 | 60 |
| 61 _WebSocketRequest(this.method, this.params) | 61 _WebSocketRequest(this.method, this.params) |
| 62 : completer = new Completer<String>(); | 62 : completer = new Completer<Map>(); |
| 63 } | 63 } |
| 64 | 64 |
| 65 /// Minimal common interface for 'WebSocket' in [dart:io] and [dart:html]. | 65 /// Minimal common interface for 'WebSocket' in [dart:io] and [dart:html]. |
| 66 abstract class CommonWebSocket { | 66 abstract class CommonWebSocket { |
| 67 void connect(String address, | 67 void connect(String address, |
| 68 void onOpen(), | 68 void onOpen(), |
| 69 void onMessage(dynamic data), | 69 void onMessage(dynamic data), |
| 70 void onError(), | 70 void onError(), |
| 71 void onClose()); | 71 void onClose()); |
| 72 bool get isOpen; | 72 bool get isOpen; |
| 73 void send(dynamic data); | 73 void send(dynamic data); |
| 74 void close(); | 74 void close(); |
| 75 Future<ByteData> nonStringToByteData(dynamic data); | 75 Future<ByteData> nonStringToByteData(dynamic data); |
| 76 } | 76 } |
| 77 | 77 |
| 78 /// A [CommonWebSocketVM] communicates with a Dart VM over a CommonWebSocket. | 78 /// A [CommonWebSocketVM] communicates with a Dart VM over a CommonWebSocket. |
| 79 /// The Dart VM can be embedded in Chromium or standalone. In the case of | 79 /// The Dart VM can be embedded in Chromium or standalone. In the case of |
| 80 /// Chromium, we make the service requests via the Chrome Remote Debugging | 80 /// Chromium, we make the service requests via the Chrome Remote Debugging |
| 81 /// Protocol. | 81 /// Protocol. |
| 82 abstract class CommonWebSocketVM extends VM { | 82 abstract class CommonWebSocketVM extends VM { |
| 83 final Completer _connected = new Completer(); | 83 final Completer _connected = new Completer(); |
| 84 final Completer _disconnected = new Completer(); | 84 final Completer _disconnected = new Completer<String>(); |
| 85 final WebSocketVMTarget target; | 85 final WebSocketVMTarget target; |
| 86 final Map<String, _WebSocketRequest> _delayedRequests = | 86 final Map<String, _WebSocketRequest> _delayedRequests = |
| 87 new Map<String, _WebSocketRequest>(); | 87 new Map<String, _WebSocketRequest>(); |
| 88 final Map<String, _WebSocketRequest> _pendingRequests = | 88 final Map<String, _WebSocketRequest> _pendingRequests = |
| 89 new Map<String, _WebSocketRequest>(); | 89 new Map<String, _WebSocketRequest>(); |
| 90 int _requestSerial = 0; | 90 int _requestSerial = 0; |
| 91 bool _hasInitiatedConnect = false; | 91 bool _hasInitiatedConnect = false; |
| 92 bool _hasFinishedConnect = false; | 92 bool _hasFinishedConnect = false; |
| 93 Utf8Decoder _utf8Decoder = new Utf8Decoder(); | 93 Utf8Decoder _utf8Decoder = new Utf8Decoder(); |
| 94 | 94 |
| 95 CommonWebSocket _webSocket; | 95 CommonWebSocket _webSocket; |
| 96 | 96 |
| 97 CommonWebSocketVM(this.target, this._webSocket) { | 97 CommonWebSocketVM(this.target, this._webSocket) { |
| 98 assert(target != null); | 98 assert(target != null); |
| 99 } | 99 } |
| 100 | 100 |
| 101 void _notifyConnect() { | 101 void _notifyConnect() { |
| 102 _hasFinishedConnect = true; | 102 _hasFinishedConnect = true; |
| 103 if (!_connected.isCompleted) { | 103 if (!_connected.isCompleted) { |
| 104 Logger.root.info('WebSocketVM connection opened: ${target.networkAddress}'
); | 104 Logger.root.info('WebSocketVM connection opened: ${target.networkAddress}'
); |
| 105 _connected.complete(this); | 105 _connected.complete(this); |
| 106 } | 106 } |
| 107 } | 107 } |
| 108 Future get onConnect => _connected.future; | 108 Future get onConnect => _connected.future; |
| 109 void _notifyDisconnect() { | 109 void _notifyDisconnect(String reason) { |
| 110 if (!_hasFinishedConnect) { | 110 if (!_hasFinishedConnect) { |
| 111 return; | 111 return; |
| 112 } | 112 } |
| 113 if (!_disconnected.isCompleted) { | 113 if (!_disconnected.isCompleted) { |
| 114 Logger.root.info('WebSocketVM connection error: ${target.networkAddress}')
; | 114 Logger.root.info('WebSocketVM connection error: ${target.networkAddress}')
; |
| 115 _disconnected.complete(this); | 115 _disconnected.complete(reason); |
| 116 } | 116 } |
| 117 } | 117 } |
| 118 Future get onDisconnect => _disconnected.future; | 118 Future get onDisconnect => _disconnected.future; |
| 119 | 119 |
| 120 void disconnect() { | 120 void disconnect({String reason : 'WebSocket closed'}) { |
| 121 if (_hasInitiatedConnect) { | 121 if (_hasInitiatedConnect) { |
| 122 _webSocket.close(); | 122 _webSocket.close(); |
| 123 } | 123 } |
| 124 _cancelAllRequests(); | 124 // We don't need to cancel requests and notify here. These |
| 125 _notifyDisconnect(); | 125 // functions will be called again when the onClose callback |
| 126 // fires. However, we may have a better 'reason' string now, so |
| 127 // let's take care of business. |
| 128 _cancelAllRequests(reason); |
| 129 _notifyDisconnect(reason); |
| 126 } | 130 } |
| 127 | 131 |
| 128 Future<String> invokeRpcRaw(String method, Map params) { | 132 Future<Map> invokeRpcRaw(String method, Map params) { |
| 129 if (!_hasInitiatedConnect) { | 133 if (!_hasInitiatedConnect) { |
| 130 _hasInitiatedConnect = true; | 134 _hasInitiatedConnect = true; |
| 131 _webSocket.connect( | 135 _webSocket.connect( |
| 132 target.networkAddress, _onOpen, _onMessage, _onError, _onClose); | 136 target.networkAddress, _onOpen, _onMessage, _onError, _onClose); |
| 133 } | 137 } |
| 134 String serial = (_requestSerial++).toString(); | 138 String serial = (_requestSerial++).toString(); |
| 135 var request = new _WebSocketRequest(method, params); | 139 var request = new _WebSocketRequest(method, params); |
| 136 if (_webSocket.isOpen) { | 140 if (_webSocket.isOpen) { |
| 137 // Already connected, send request immediately. | 141 // Already connected, send request immediately. |
| 138 _sendRequest(serial, request); | 142 _sendRequest(serial, request); |
| 139 } else { | 143 } else { |
| 140 // Not connected yet, add to delayed requests. | 144 // Not connected yet, add to delayed requests. |
| 141 _delayedRequests[serial] = request; | 145 _delayedRequests[serial] = request; |
| 142 } | 146 } |
| 143 return request.completer.future; | 147 return request.completer.future; |
| 144 } | 148 } |
| 145 | 149 |
| 146 void _onClose() { | 150 void _onClose() { |
| 147 _cancelAllRequests(); | 151 _cancelAllRequests('WebSocket closed'); |
| 148 _notifyDisconnect(); | 152 _notifyDisconnect('WebSocket closed'); |
| 149 } | 153 } |
| 150 | 154 |
| 151 // WebSocket error event handler. | 155 // WebSocket error event handler. |
| 152 void _onError() { | 156 void _onError() { |
| 153 _cancelAllRequests(); | 157 // TODO(turnidge): The implementors of CommonWebSocket have more |
| 154 _notifyDisconnect(); | 158 // error information available. Consider providing that here. |
| 159 _cancelAllRequests('WebSocket closed due to error'); |
| 160 _notifyDisconnect('WebSocket closed due to error'); |
| 155 } | 161 } |
| 156 | 162 |
| 157 // WebSocket open event handler. | 163 // WebSocket open event handler. |
| 158 void _onOpen() { | 164 void _onOpen() { |
| 159 target.lastConnectionTime = new DateTime.now().millisecondsSinceEpoch; | 165 target.lastConnectionTime = new DateTime.now().millisecondsSinceEpoch; |
| 160 _sendAllDelayedRequests(); | 166 _sendAllDelayedRequests(); |
| 161 _notifyConnect(); | 167 _notifyConnect(); |
| 162 } | 168 } |
| 163 | 169 |
| 170 Map _parseJSON(String message) { |
| 171 var map; |
| 172 try { |
| 173 map = JSON.decode(message); |
| 174 } catch (e, st) { |
| 175 Logger.root.severe('Disconnecting: Error decoding message: $e\n$st'); |
| 176 disconnect(reason:'Error decoding JSON message: $e'); |
| 177 return null; |
| 178 } |
| 179 if (map == null) { |
| 180 Logger.root.severe("Disconnecting: Unable to decode 'null' message"); |
| 181 disconnect(reason:"Unable to decode 'null' message"); |
| 182 return null; |
| 183 } |
| 184 return map; |
| 185 } |
| 186 |
| 164 void _onBinaryMessage(dynamic data) { | 187 void _onBinaryMessage(dynamic data) { |
| 165 _webSocket.nonStringToByteData(data).then((ByteData bytes) { | 188 _webSocket.nonStringToByteData(data).then((ByteData bytes) { |
| 166 // See format spec. in VMs Service::SendEvent. | 189 // See format spec. in VMs Service::SendEvent. |
| 167 int offset = 0; | 190 int offset = 0; |
| 168 // Dart2JS workaround (no getUint64). Limit to 4 GB metadata. | 191 // Dart2JS workaround (no getUint64). Limit to 4 GB metadata. |
| 169 assert(bytes.getUint32(offset, Endianness.BIG_ENDIAN) == 0); | 192 assert(bytes.getUint32(offset, Endianness.BIG_ENDIAN) == 0); |
| 170 int metaSize = bytes.getUint32(offset + 4, Endianness.BIG_ENDIAN); | 193 int metaSize = bytes.getUint32(offset + 4, Endianness.BIG_ENDIAN); |
| 171 offset += 8; | 194 offset += 8; |
| 172 var meta = _utf8Decoder.convert(new Uint8List.view( | 195 var meta = _utf8Decoder.convert(new Uint8List.view( |
| 173 bytes.buffer, bytes.offsetInBytes + offset, metaSize)); | 196 bytes.buffer, bytes.offsetInBytes + offset, metaSize)); |
| 174 offset += metaSize; | 197 offset += metaSize; |
| 175 var data = new ByteData.view( | 198 var data = new ByteData.view( |
| 176 bytes.buffer, | 199 bytes.buffer, |
| 177 bytes.offsetInBytes + offset, | 200 bytes.offsetInBytes + offset, |
| 178 bytes.lengthInBytes - offset); | 201 bytes.lengthInBytes - offset); |
| 179 postServiceEvent(meta, data); | 202 var map = _parseJSON(meta); |
| 203 if (map == null) { |
| 204 return; |
| 205 } |
| 206 var event = map['event']; |
| 207 postServiceEvent(event, data); |
| 180 }); | 208 }); |
| 181 } | 209 } |
| 182 | 210 |
| 183 void _onStringMessage(String data) { | 211 void _onStringMessage(String data) { |
| 184 var map = JSON.decode(data); | 212 var map = _parseJSON(data); |
| 185 if (map == null) { | 213 if (map == null) { |
| 186 Logger.root.severe('WebSocketVM got empty message'); | |
| 187 return; | 214 return; |
| 188 } | 215 } |
| 189 // Extract serial and result. | 216 var event = map['event']; |
| 190 var serial; | 217 if (event != null) { |
| 191 var result; | 218 postServiceEvent(event, null); |
| 192 serial = map['id']; | |
| 193 result = map['result']; | |
| 194 if (serial == null) { | |
| 195 // Messages without serial numbers are asynchronous events | |
| 196 // from the vm. | |
| 197 postServiceEvent(result, null); | |
| 198 return; | 219 return; |
| 199 } | 220 } |
| 221 |
| 222 // Extract serial and result. |
| 223 var serial = map['id']; |
| 224 var result = map['result']; |
| 225 |
| 200 // Complete request. | 226 // Complete request. |
| 201 var request = _pendingRequests.remove(serial); | 227 var request = _pendingRequests.remove(serial); |
| 202 if (request == null) { | 228 if (request == null) { |
| 203 Logger.root.severe('Received unexpected message: ${map}'); | 229 Logger.root.severe('Received unexpected message: ${map}'); |
| 204 return; | 230 return; |
| 205 } | 231 } |
| 206 if (request.method != 'getTagProfile' && | 232 if (request.method != 'getTagProfile' && |
| 207 request.method != 'getIsolateMetric' && | 233 request.method != 'getIsolateMetric' && |
| 208 request.method != 'getVMMetric') { | 234 request.method != 'getVMMetric') { |
| 209 Logger.root.info( | 235 Logger.root.info( |
| 210 'RESPONSE [${serial}] ${request.method}'); | 236 'RESPONSE [${serial}] ${request.method}'); |
| 211 } | 237 } |
| 212 request.completer.complete(result); | 238 request.completer.complete(result); |
| 213 } | 239 } |
| 214 | 240 |
| 215 // WebSocket message event handler. | 241 // WebSocket message event handler. |
| 216 void _onMessage(dynamic data) { | 242 void _onMessage(dynamic data) { |
| 217 if (data is! String) { | 243 if (data is! String) { |
| 218 _onBinaryMessage(data); | 244 _onBinaryMessage(data); |
| 219 } else { | 245 } else { |
| 220 _onStringMessage(data); | 246 _onStringMessage(data); |
| 221 } | 247 } |
| 222 } | 248 } |
| 223 | 249 |
| 224 String _generateNetworkError(String userMessage) { | 250 Map _generateNetworkError(String userMessage) { |
| 225 return JSON.encode({ | 251 var response = { |
| 226 'type': 'ServiceException', | 252 'type': 'ServiceException', |
| 227 'id': '', | 253 'kind': 'ConnectionClosed', |
| 228 'kind': 'NetworkException', | 254 'message': userMessage, |
| 229 'message': userMessage | 255 }; |
| 230 }); | 256 return response; |
| 231 } | 257 } |
| 232 | 258 |
| 233 void _cancelRequests(Map<String, _WebSocketRequest> requests) { | 259 void _cancelRequests(Map<String,_WebSocketRequest> requests, |
| 234 requests.forEach((String serial, _WebSocketRequest request) { | 260 String reason) { |
| 261 requests.forEach((_, _WebSocketRequest request) { |
| 235 request.completer.complete( | 262 request.completer.complete( |
| 236 _generateNetworkError('WebSocket disconnected')); | 263 _generateNetworkError(reason)); |
| 237 }); | 264 }); |
| 238 requests.clear(); | 265 requests.clear(); |
| 239 } | 266 } |
| 240 | 267 |
| 241 /// Cancel all pending and delayed requests by completing them with an error. | 268 /// Cancel all pending and delayed requests by completing them with an error. |
| 242 void _cancelAllRequests() { | 269 void _cancelAllRequests(String reason) { |
| 243 if (_pendingRequests.length > 0) { | 270 if (_pendingRequests.length > 0) { |
| 244 Logger.root.info('Cancelling all pending requests.'); | 271 Logger.root.info('Cancelling all pending requests.'); |
| 245 _cancelRequests(_pendingRequests); | 272 _cancelRequests(_pendingRequests, reason); |
| 246 } | 273 } |
| 247 if (_delayedRequests.length > 0) { | 274 if (_delayedRequests.length > 0) { |
| 248 Logger.root.info('Cancelling all delayed requests.'); | 275 Logger.root.info('Cancelling all delayed requests.'); |
| 249 _cancelRequests(_delayedRequests); | 276 _cancelRequests(_delayedRequests, reason); |
| 250 } | 277 } |
| 251 } | 278 } |
| 252 | 279 |
| 253 /// Send all delayed requests. | 280 /// Send all delayed requests. |
| 254 void _sendAllDelayedRequests() { | 281 void _sendAllDelayedRequests() { |
| 255 assert(_webSocket.isOpen); | 282 assert(_webSocket.isOpen); |
| 256 if (_delayedRequests.length == 0) { | 283 if (_delayedRequests.length == 0) { |
| 257 return; | 284 return; |
| 258 } | 285 } |
| 259 Logger.root.info('Sending all delayed requests.'); | 286 Logger.root.info('Sending all delayed requests.'); |
| (...skipping 28 matching lines...) Expand all Loading... |
| 288 if (request.method != 'getTagProfile' && | 315 if (request.method != 'getTagProfile' && |
| 289 request.method != 'getIsolateMetric' && | 316 request.method != 'getIsolateMetric' && |
| 290 request.method != 'getVMMetric') { | 317 request.method != 'getVMMetric') { |
| 291 Logger.root.info( | 318 Logger.root.info( |
| 292 'GET [${serial}] ${request.method} from ${target.networkAddress}'); | 319 'GET [${serial}] ${request.method} from ${target.networkAddress}'); |
| 293 } | 320 } |
| 294 // Send message. | 321 // Send message. |
| 295 _webSocket.send(message); | 322 _webSocket.send(message); |
| 296 } | 323 } |
| 297 } | 324 } |
| OLD | NEW |