| // 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 EntryPoint = void Function(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. | |
| final SendPort _sendPort; | |
| /// The port used to receive requests from the server. | |
| late final 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 = ReceivePort(); | |
| _sendPort.send(_receivePort.sendPort); | |
| } | |
| @override | |
| void close() { | |
| _receivePort.close(); | |
| _subscription?.cancel(); | |
| _subscription = null; | |
| } | |
| @override | |
| void listen( | |
| void Function(Request request) onRequest, { | |
| Function? onError, | |
| void Function()? onDone, | |
| }) { | |
| void onData(data) { | |
| var requestMap = data as Map<String, Object>; | |
| var request = Request.fromJson(requestMap); | |
| onRequest(request); | |
| } | |
| if (_subscription != null) { | |
| throw StateError('Only one listener is allowed per channel'); | |
| } | |
| // Note: The documentation for [ReceivePort.listen] indicates that | |
| // `onError` and `cancelOnError` are always ignored, "since a ReceivePort | |
| // will never receive an error." It is potentially confusing to pass them | |
| // here, as if they had an effect. Consider removing. | |
| _subscription = _receivePort.listen( | |
| onData, | |
| onError: onError, | |
| onDone: onDone, | |
| cancelOnError: false, | |
| ); | |
| } | |
| @override | |
| void sendNotification(Notification notification) { | |
| var json = notification.toJson(); | |
| _sendPort.send(json); | |
| } | |
| @override | |
| void sendResponse(Response response) { | |
| var 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 Function(Response response) onResponse, | |
| void Function(Notification notification) onNotification, { | |
| void Function(dynamic error)? onError, | |
| void Function()? onDone, | |
| }) async { | |
| if (_isolate != null) { | |
| throw StateError('Cannot listen to the same channel more than once.'); | |
| } | |
| var receivePort = ReceivePort(); | |
| _receivePort = receivePort; | |
| if (onError != null) { | |
| var errorPort = ReceivePort(); | |
| _errorPort = errorPort; | |
| errorPort.listen((error) { | |
| onError(error); | |
| }); | |
| } | |
| if (onDone != null) { | |
| var exitPort = ReceivePort(); | |
| _exitPort = exitPort; | |
| exitPort.listen((_) { | |
| onDone(); | |
| }); | |
| } | |
| try { | |
| _isolate = await _spawnIsolate(); | |
| } catch (exception, stackTrace) { | |
| instrumentationService.logPluginError( | |
| 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; | |
| } | |
| var channelReady = Completer<void>(); | |
| receivePort.listen((dynamic input) { | |
| if (input is SendPort) { | |
| _sendPort = input; | |
| channelReady.complete(null); | |
| } else if (input is Map<String, Object?>) { | |
| if (input.containsKey('id')) { | |
| var encodedInput = json.encode(input); | |
| instrumentationService.logPluginResponse(pluginId, encodedInput); | |
| onResponse(Response.fromJson(input)); | |
| } else if (input.containsKey('event')) { | |
| var encodedInput = json.encode(input); | |
| instrumentationService.logPluginNotification(pluginId, encodedInput); | |
| onNotification(Notification.fromJson(input)); | |
| } | |
| } | |
| }); | |
| return channelReady.future; | |
| } | |
| @override | |
| void sendRequest(Request request) { | |
| var sendPort = _sendPort; | |
| if (sendPort != null) { | |
| var jsonData = request.toJson(); | |
| var encodedRequest = json.encode(jsonData); | |
| instrumentationService.logPluginRequest(pluginId, encodedRequest); | |
| sendPort.send(jsonData); | |
| } | |
| } | |
| /// Spawn the isolate in which the plugin is running. | |
| Future<Isolate> _spawnIsolate(); | |
| } |