|
| 1 | +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file |
| 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. |
| 4 | + |
| 5 | +import 'dart:async'; |
| 6 | +import 'dart:convert'; |
| 7 | +import 'dart:isolate'; |
| 8 | + |
| 9 | +import 'package:analysis_server/src/session_logger/process_id.dart'; |
| 10 | +import 'package:analysis_server/src/session_logger/session_logger.dart'; |
| 11 | +import 'package:analyzer/instrumentation/instrumentation.dart'; |
| 12 | +import 'package:analyzer_plugin/channel/channel.dart'; |
| 13 | +import 'package:analyzer_plugin/protocol/protocol.dart'; |
| 14 | +import 'package:analyzer_plugin/protocol/protocol_generated.dart'; |
| 15 | + |
| 16 | +/// The type of the function used to run a built-in plugin in an isolate. |
| 17 | +typedef EntryPoint = void Function(SendPort sendPort); |
| 18 | + |
| 19 | +/// A communication channel appropriate for built-in plugins. |
| 20 | +class BuiltInServerIsolateChannel extends ServerIsolateChannel { |
| 21 | + /// The entry point |
| 22 | + final EntryPoint entryPoint; |
| 23 | + |
| 24 | + @override |
| 25 | + final String pluginId; |
| 26 | + |
| 27 | + /// Initialize a newly created channel to communicate with an isolate running |
| 28 | + /// the given [entryPoint]. |
| 29 | + BuiltInServerIsolateChannel( |
| 30 | + this.entryPoint, |
| 31 | + this.pluginId, |
| 32 | + InstrumentationService instrumentationService, |
| 33 | + SessionLogger sessionLogger, |
| 34 | + ) : super._(instrumentationService, sessionLogger); |
| 35 | + |
| 36 | + @override |
| 37 | + Future<Isolate> _spawnIsolate() { |
| 38 | + return Isolate.spawn( |
| 39 | + (message) => entryPoint(message as SendPort), |
| 40 | + _receivePort?.sendPort, |
| 41 | + onError: _errorPort?.sendPort, |
| 42 | + onExit: _exitPort?.sendPort, |
| 43 | + ); |
| 44 | + } |
| 45 | +} |
| 46 | + |
| 47 | +/// A communication channel appropriate for discovered plugins. |
| 48 | +class DiscoveredServerIsolateChannel extends ServerIsolateChannel { |
| 49 | + /// The URI for the Dart file that will be run in the isolate that this |
| 50 | + /// channel communicates with. |
| 51 | + final Uri pluginUri; |
| 52 | + |
| 53 | + /// The URI for the '.packages' file that will control how 'package:' URIs are |
| 54 | + /// resolved. |
| 55 | + final Uri packagesUri; |
| 56 | + |
| 57 | + /// Initialize a newly created channel to communicate with an isolate running |
| 58 | + /// the code at the given [uri]. |
| 59 | + DiscoveredServerIsolateChannel( |
| 60 | + this.pluginUri, |
| 61 | + this.packagesUri, |
| 62 | + InstrumentationService instrumentationService, |
| 63 | + SessionLogger sessionLogger, |
| 64 | + ) : super._(instrumentationService, sessionLogger); |
| 65 | + |
| 66 | + @override |
| 67 | + String get pluginId => pluginUri.toString(); |
| 68 | + |
| 69 | + @override |
| 70 | + Future<Isolate> _spawnIsolate() { |
| 71 | + return Isolate.spawnUri( |
| 72 | + pluginUri, |
| 73 | + <String>[], |
| 74 | + _receivePort?.sendPort, |
| 75 | + onError: _errorPort?.sendPort, |
| 76 | + onExit: _exitPort?.sendPort, |
| 77 | + packageConfig: packagesUri, |
| 78 | + ); |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +/// A communication channel that allows an analysis server to send [Request]s |
| 83 | +/// to, and to receive both [Response]s and [Notification]s from, a plugin. |
| 84 | +abstract class ServerIsolateChannel implements ServerCommunicationChannel { |
| 85 | + /// The instrumentation service that is being used by the analysis server. |
| 86 | + final InstrumentationService instrumentationService; |
| 87 | + |
| 88 | + /// The session logger that is to be used by this channel. |
| 89 | + final SessionLogger sessionLogger; |
| 90 | + |
| 91 | + /// The isolate in which the plugin is running, or `null` if the plugin has |
| 92 | + /// not yet been started by invoking [listen]. |
| 93 | + Isolate? _isolate; |
| 94 | + |
| 95 | + /// The port used to send requests to the plugin, or `null` if the plugin has |
| 96 | + /// not yet been started by invoking [listen]. |
| 97 | + SendPort? _sendPort; |
| 98 | + |
| 99 | + /// The port used to receive responses and notifications from the plugin. |
| 100 | + ReceivePort? _receivePort; |
| 101 | + |
| 102 | + /// The port used to receive unhandled exceptions thrown in the plugin. |
| 103 | + ReceivePort? _errorPort; |
| 104 | + |
| 105 | + /// The port used to receive notification when the plugin isolate has exited. |
| 106 | + ReceivePort? _exitPort; |
| 107 | + |
| 108 | + /// Return a communication channel appropriate for communicating with a |
| 109 | + /// built-in plugin. |
| 110 | + // TODO(brianwilkerson): This isn't currently referenced. We should decide |
| 111 | + // whether there's any value to supporting built-in plugins and remove this |
| 112 | + // if we decide there isn't. |
| 113 | + factory ServerIsolateChannel.builtIn( |
| 114 | + EntryPoint entryPoint, |
| 115 | + String pluginId, |
| 116 | + InstrumentationService instrumentationService, |
| 117 | + SessionLogger sessionLogger, |
| 118 | + ) = BuiltInServerIsolateChannel; |
| 119 | + |
| 120 | + /// Return a communication channel appropriate for communicating with a |
| 121 | + /// discovered plugin. |
| 122 | + factory ServerIsolateChannel.discovered( |
| 123 | + Uri pluginUri, |
| 124 | + Uri packagesUri, |
| 125 | + InstrumentationService instrumentationService, |
| 126 | + SessionLogger sessionLogger, |
| 127 | + ) = DiscoveredServerIsolateChannel; |
| 128 | + |
| 129 | + /// Initialize a newly created channel. |
| 130 | + ServerIsolateChannel._(this.instrumentationService, this.sessionLogger); |
| 131 | + |
| 132 | + /// Return the id of the plugin running in the isolate, used to identify the |
| 133 | + /// plugin to the instrumentation service. |
| 134 | + String get pluginId; |
| 135 | + |
| 136 | + @override |
| 137 | + void close() { |
| 138 | + _receivePort?.close(); |
| 139 | + _errorPort?.close(); |
| 140 | + _exitPort?.close(); |
| 141 | + _isolate = null; |
| 142 | + } |
| 143 | + |
| 144 | + @override |
| 145 | + void kill() { |
| 146 | + _isolate?.kill(priority: Isolate.immediate); |
| 147 | + } |
| 148 | + |
| 149 | + @override |
| 150 | + Future<void> listen( |
| 151 | + void Function(Response response) onResponse, |
| 152 | + void Function(Notification notification) onNotification, { |
| 153 | + void Function(dynamic error)? onError, |
| 154 | + void Function()? onDone, |
| 155 | + }) async { |
| 156 | + if (_isolate != null) { |
| 157 | + throw StateError('Cannot listen to the same channel more than once.'); |
| 158 | + } |
| 159 | + |
| 160 | + var receivePort = ReceivePort(); |
| 161 | + _receivePort = receivePort; |
| 162 | + |
| 163 | + if (onError != null) { |
| 164 | + var errorPort = ReceivePort(); |
| 165 | + _errorPort = errorPort; |
| 166 | + errorPort.listen((error) { |
| 167 | + onError(error); |
| 168 | + }); |
| 169 | + } |
| 170 | + |
| 171 | + if (onDone != null) { |
| 172 | + var exitPort = ReceivePort(); |
| 173 | + _exitPort = exitPort; |
| 174 | + exitPort.listen((_) { |
| 175 | + onDone(); |
| 176 | + }); |
| 177 | + } |
| 178 | + |
| 179 | + try { |
| 180 | + _isolate = await _spawnIsolate(); |
| 181 | + } catch (exception, stackTrace) { |
| 182 | + instrumentationService.logPluginError( |
| 183 | + PluginData(pluginId, null, null), |
| 184 | + RequestErrorCode.PLUGIN_ERROR.toString(), |
| 185 | + exception.toString(), |
| 186 | + stackTrace.toString(), |
| 187 | + ); |
| 188 | + if (onError != null) { |
| 189 | + onError([exception.toString(), stackTrace.toString()]); |
| 190 | + } |
| 191 | + if (onDone != null) { |
| 192 | + onDone(); |
| 193 | + } |
| 194 | + close(); |
| 195 | + return; |
| 196 | + } |
| 197 | + |
| 198 | + var channelReady = Completer<void>(); |
| 199 | + receivePort.listen((dynamic input) { |
| 200 | + if (input is SendPort) { |
| 201 | + _sendPort = input; |
| 202 | + channelReady.complete(null); |
| 203 | + } else if (input is Map<String, Object?>) { |
| 204 | + if (input.containsKey('id')) { |
| 205 | + var encodedInput = json.encode(input); |
| 206 | + instrumentationService.logPluginResponse(pluginId, encodedInput); |
| 207 | + sessionLogger.logMessage( |
| 208 | + from: ProcessId.plugin, |
| 209 | + to: ProcessId.server, |
| 210 | + message: input, |
| 211 | + ); |
| 212 | + onResponse(Response.fromJson(input)); |
| 213 | + } else if (input.containsKey('event')) { |
| 214 | + var encodedInput = json.encode(input); |
| 215 | + instrumentationService.logPluginNotification(pluginId, encodedInput); |
| 216 | + sessionLogger.logMessage( |
| 217 | + from: ProcessId.plugin, |
| 218 | + to: ProcessId.server, |
| 219 | + message: input, |
| 220 | + ); |
| 221 | + onNotification(Notification.fromJson(input)); |
| 222 | + } |
| 223 | + } |
| 224 | + }); |
| 225 | + |
| 226 | + return channelReady.future; |
| 227 | + } |
| 228 | + |
| 229 | + @override |
| 230 | + void sendRequest(Request request) { |
| 231 | + var sendPort = _sendPort; |
| 232 | + if (sendPort != null) { |
| 233 | + var jsonData = request.toJson(); |
| 234 | + var encodedRequest = json.encode(jsonData); |
| 235 | + instrumentationService.logPluginRequest(pluginId, encodedRequest); |
| 236 | + sessionLogger.logMessage( |
| 237 | + from: ProcessId.server, |
| 238 | + to: ProcessId.plugin, |
| 239 | + message: jsonData, |
| 240 | + ); |
| 241 | + sendPort.send(jsonData); |
| 242 | + } |
| 243 | + } |
| 244 | + |
| 245 | + /// Spawn the isolate in which the plugin is running. |
| 246 | + Future<Isolate> _spawnIsolate(); |
| 247 | +} |
0 commit comments