// Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'dart:io'; import 'package:json_rpc_2/json_rpc_2.dart' as rpc; import 'package:web_socket_channel/io.dart'; class Observatory { Observatory._(this.peer, this.port) { peer.registerMethod('streamNotify', (rpc.Parameters event) { _handleStreamNotify(event.asMap); }); onIsolateEvent.listen((Event event) { if (event.kind == 'IsolateStart') { _addIsolate(event.isolate); } else if (event.kind == 'IsolateExit') { String removedId = event.isolate.id; isolates.removeWhere((IsolateRef ref) => ref.id == removedId); } }); } static Future connect(int port) async { Uri uri = new Uri(scheme: 'ws', host: '127.0.0.1', port: port, path: 'ws'); WebSocket ws = await WebSocket.connect(uri.toString()); rpc.Peer peer = new rpc.Peer(new IOWebSocketChannel(ws)); peer.listen(); return new Observatory._(peer, port); } final rpc.Peer peer; final int port; List isolates = []; Completer _waitFirstIsolateCompleter; Map> _eventControllers = >{}; Set _listeningFor = new Set(); bool get isClosed => peer.isClosed; Future get done => peer.done; String get firstIsolateId => isolates.isEmpty ? null : isolates.first.id; // Events Stream get onExtensionEvent => onEvent('Extension'); // IsolateStart, IsolateRunnable, IsolateExit, IsolateUpdate, ServiceExtensionAdded Stream get onIsolateEvent => onEvent('Isolate'); Stream get onTimelineEvent => onEvent('Timeline'); // Listen for a specific event name. Stream onEvent(String streamId) { streamListen(streamId); return _getEventController(streamId).stream; } StreamController _getEventController(String eventName) { StreamController controller = _eventControllers[eventName]; if (controller == null) { controller = new StreamController.broadcast(); _eventControllers[eventName] = controller; } return controller; } void _handleStreamNotify(Map data) { Event event = new Event(data['event']); _getEventController(data['streamId']).add(event); } Future get waitFirstIsolate async { if (isolates.isNotEmpty) return isolates.first; _waitFirstIsolateCompleter = new Completer(); getVM().then((VM vm) { for (IsolateRef isolate in vm.isolates) _addIsolate(isolate); }); return _waitFirstIsolateCompleter.future; } // Requests Future sendRequest(String method, [Map args]) { return peer.sendRequest(method, args).then((dynamic result) => new Response(result)); } Future streamListen(String streamId) async { if (!_listeningFor.contains(streamId)) { _listeningFor.add(streamId); sendRequest('streamListen', { 'streamId': streamId }); } } Future getVM() { return peer.sendRequest('getVM').then((dynamic result) { return new VM(result); }); } Future isolateReload(String isolateId) { return sendRequest('isolateReload', { 'isolateId': isolateId }); } Future clearVMTimeline() => sendRequest('_clearVMTimeline'); Future setVMTimelineFlags(List recordedStreams) { assert(recordedStreams != null); return sendRequest('_setVMTimelineFlags', { 'recordedStreams': recordedStreams }); } Future getVMTimeline() => sendRequest('_getVMTimeline'); // Flutter extension methods. Future flutterExit(String isolateId) { return peer.sendRequest('ext.flutter.exit', { 'isolateId': isolateId }).then((dynamic result) => new Response(result)); } void _addIsolate(IsolateRef isolate) { if (!isolates.contains(isolate)) { isolates.add(isolate); if (_waitFirstIsolateCompleter != null) { _waitFirstIsolateCompleter.complete(isolate); _waitFirstIsolateCompleter = null; } } } } class Response { Response(this.response); final Map response; String get type => response['type']; dynamic operator[](String key) => response[key]; @override String toString() => response.toString(); } class VM extends Response { VM(Map response) : super(response); List get isolates => response['isolates'].map((dynamic ref) => new IsolateRef(ref)).toList(); } class Event extends Response { Event(Map response) : super(response); String get kind => response['kind']; IsolateRef get isolate => new IsolateRef.from(response['isolate']); /// Only valid for [kind] == `Extension`. String get extensionKind => response['extensionKind']; } class IsolateRef extends Response { IsolateRef(Map response) : super(response); factory IsolateRef.from(dynamic ref) => ref == null ? null : new IsolateRef(ref); String get id => response['id']; @override bool operator ==(dynamic other) { if (identical(this, other)) return true; if (other is! IsolateRef) return false; final IsolateRef typedOther = other; return id == typedOther.id; } @override int get hashCode => id.hashCode; }