| // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file |
| // for details. 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:convert'; |
| import 'dart:isolate'; |
| |
| import 'package:analyzer/instrumentation/instrumentation.dart'; |
| import 'package:analyzer_plugin/channel/channel.dart'; |
| import 'package:analyzer_plugin/protocol/protocol.dart'; |
| import 'package:analyzer_plugin/protocol/protocol_generated.dart'; |
| |
| /** |
| * The type of the function used to run a built-in plugin in an isolate. |
| */ |
| typedef void EntryPoint(SendPort sendPort); |
| |
| /** |
| * A communication channel appropriate for built-in plugins. |
| */ |
| class BuiltInServerIsolateChannel extends ServerIsolateChannel { |
| /** |
| * The entry point |
| */ |
| final EntryPoint entryPoint; |
| |
| @override |
| final String pluginId; |
| |
| /** |
| * Initialize a newly created channel to communicate with an isolate running |
| * the given [entryPoint]. |
| */ |
| BuiltInServerIsolateChannel(this.entryPoint, this.pluginId, |
| InstrumentationService instrumentationService) |
| : super._(instrumentationService); |
| |
| @override |
| Future<Isolate> _spawnIsolate() { |
| return Isolate.spawn( |
| (message) => entryPoint(message as SendPort), _receivePort.sendPort, |
| onError: _errorPort?.sendPort, onExit: _exitPort?.sendPort); |
| } |
| } |
| |
| /** |
| * A communication channel appropriate for discovered plugins. |
| */ |
| class DiscoveredServerIsolateChannel extends ServerIsolateChannel { |
| /** |
| * The URI for the Dart file that will be run in the isolate that this channel |
| * communicates with. |
| */ |
| final Uri pluginUri; |
| |
| /** |
| * The URI for the '.packages' file that will control how 'package:' URIs are |
| * resolved. |
| */ |
| final Uri packagesUri; |
| |
| /** |
| * Initialize a newly created channel to communicate with an isolate running |
| * the code at the given [uri]. |
| */ |
| DiscoveredServerIsolateChannel(this.pluginUri, this.packagesUri, |
| InstrumentationService instrumentationService) |
| : super._(instrumentationService); |
| |
| @override |
| String get pluginId => pluginUri.toString(); |
| |
| @override |
| Future<Isolate> _spawnIsolate() { |
| return Isolate.spawnUri(pluginUri, <String>[], _receivePort.sendPort, |
| onError: _errorPort?.sendPort, |
| onExit: _exitPort?.sendPort, |
| packageConfig: packagesUri); |
| } |
| } |
| |
| /** |
| * The object that allows a [ServerPlugin] to receive [Request]s and to return |
| * both [Response]s and [Notification]s. It communicates with the analysis |
| * server by passing data to the server's main isolate. |
| */ |
| class PluginIsolateChannel implements PluginCommunicationChannel { |
| /** |
| * The port used to send notifications and responses to the server. |
| */ |
| SendPort _sendPort; |
| |
| /** |
| * The port used to receive requests from the server. |
| */ |
| ReceivePort _receivePort; |
| |
| /** |
| * The subscription that needs to be cancelled when the channel is closed. |
| */ |
| StreamSubscription _subscription; |
| |
| /** |
| * Initialize a newly created channel to communicate with the server. |
| */ |
| PluginIsolateChannel(this._sendPort) { |
| _receivePort = new ReceivePort(); |
| _sendPort.send(_receivePort.sendPort); |
| } |
| |
| @override |
| void close() { |
| if (_subscription != null) { |
| _subscription.cancel(); |
| _subscription = null; |
| } |
| } |
| |
| @override |
| void listen(void onRequest(Request request), |
| {Function onError, void onDone()}) { |
| void onData(data) { |
| Map<String, Object> requestMap = data as Map<String, Object>; |
| Request request = new Request.fromJson(requestMap); |
| if (request != null) { |
| onRequest(request); |
| } |
| } |
| |
| if (_subscription != null) { |
| throw new StateError('Only one listener is allowed per channel'); |
| } |
| _subscription = _receivePort.listen(onData, |
| onError: onError, onDone: onDone, cancelOnError: false); |
| } |
| |
| @override |
| void sendNotification(Notification notification) { |
| Map<String, Object> json = notification.toJson(); |
| _sendPort.send(json); |
| } |
| |
| @override |
| void sendResponse(Response response) { |
| Map<String, Object> json = response.toJson(); |
| _sendPort.send(json); |
| } |
| } |
| |
| /** |
| * A communication channel that allows an analysis server to send [Request]s |
| * to, and to receive both [Response]s and [Notification]s from, a plugin. |
| */ |
| abstract class ServerIsolateChannel implements ServerCommunicationChannel { |
| /** |
| * The instrumentation service that is being used by the analysis server. |
| */ |
| final InstrumentationService instrumentationService; |
| |
| /** |
| * The isolate in which the plugin is running, or `null` if the plugin has |
| * not yet been started by invoking [listen]. |
| */ |
| Isolate _isolate; |
| |
| /** |
| * The port used to send requests to the plugin, or `null` if the plugin has |
| * not yet been started by invoking [listen]. |
| */ |
| SendPort _sendPort; |
| |
| /** |
| * The port used to receive responses and notifications from the plugin. |
| */ |
| ReceivePort _receivePort; |
| |
| /** |
| * The port used to receive unhandled exceptions thrown in the plugin. |
| */ |
| ReceivePort _errorPort; |
| |
| /** |
| * The port used to receive notification when the plugin isolate has exited. |
| */ |
| ReceivePort _exitPort; |
| |
| /** |
| * Return a communication channel appropriate for communicating with a |
| * built-in plugin. |
| */ |
| factory ServerIsolateChannel.builtIn(EntryPoint entryPoint, String pluginId, |
| InstrumentationService instrumentationService) = |
| BuiltInServerIsolateChannel; |
| |
| /** |
| * Return a communication channel appropriate for communicating with a |
| * discovered plugin. |
| */ |
| factory ServerIsolateChannel.discovered(Uri pluginUri, Uri packagesUri, |
| InstrumentationService instrumentationService) = |
| DiscoveredServerIsolateChannel; |
| |
| /** |
| * Initialize a newly created channel. |
| */ |
| ServerIsolateChannel._(this.instrumentationService); |
| |
| /** |
| * Return the id of the plugin running in the isolate, used to identify the |
| * plugin to the instrumentation service. |
| */ |
| String get pluginId; |
| |
| @override |
| void close() { |
| _receivePort?.close(); |
| _errorPort?.close(); |
| _exitPort?.close(); |
| _isolate = null; |
| } |
| |
| @override |
| void kill() { |
| _isolate?.kill(priority: Isolate.immediate); |
| } |
| |
| @override |
| Future<void> listen(void onResponse(Response response), |
| void onNotification(Notification notification), |
| {void onError(dynamic error), void onDone()}) async { |
| // TODO(brianwilkerson) Determine whether this await is necessary. |
| await null; |
| if (_isolate != null) { |
| throw new StateError('Cannot listen to the same channel more than once.'); |
| } |
| _receivePort = new ReceivePort(); |
| if (onError != null) { |
| _errorPort = new ReceivePort(); |
| _errorPort.listen((error) { |
| onError(error); |
| }); |
| } |
| if (onDone != null) { |
| _exitPort = new ReceivePort(); |
| _exitPort.listen((_) { |
| onDone(); |
| }); |
| } |
| try { |
| _isolate = await _spawnIsolate(); |
| } catch (exception, stackTrace) { |
| instrumentationService.logPluginError( |
| new PluginData(pluginId, null, null), |
| RequestErrorCode.PLUGIN_ERROR.toString(), |
| exception.toString(), |
| stackTrace.toString()); |
| if (onError != null) { |
| onError([exception.toString(), stackTrace.toString()]); |
| } |
| if (onDone != null) { |
| onDone(); |
| } |
| close(); |
| return null; |
| } |
| Completer<void> channelReady = new Completer<void>(); |
| _receivePort.listen((dynamic input) { |
| if (input is SendPort) { |
| _sendPort = input; |
| channelReady.complete(null); |
| } else if (input is Map) { |
| if (input.containsKey('id')) { |
| String encodedInput = json.encode(input); |
| instrumentationService.logPluginResponse(pluginId, encodedInput); |
| onResponse(new Response.fromJson(input)); |
| } else if (input.containsKey('event')) { |
| String encodedInput = json.encode(input); |
| instrumentationService.logPluginNotification(pluginId, encodedInput); |
| onNotification(new Notification.fromJson(input)); |
| } |
| } |
| }); |
| return channelReady.future; |
| } |
| |
| @override |
| void sendRequest(Request request) { |
| if (_sendPort != null) { |
| Map<String, Object> jsonData = request.toJson(); |
| String encodedRequest = json.encode(jsonData); |
| instrumentationService.logPluginRequest(pluginId, encodedRequest); |
| _sendPort.send(jsonData); |
| } |
| } |
| |
| /** |
| * Spawn the isolate in which the plugin is running. |
| */ |
| Future<Isolate> _spawnIsolate(); |
| } |