Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(133)

Side by Side Diff: pkg/analysis_server/lib/src/plugin/plugin_manager.dart

Issue 2746293004: Add an object that can manage which plugins are associated with each context / driver (Closed)
Patch Set: Created 3 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | pkg/analysis_server/test/src/plugin/plugin_manager_test.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
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:collection';
7 import 'dart:io' show Platform;
8
9 import 'package:analysis_server/src/plugin/notification_manager.dart';
10 import 'package:analyzer/file_system/file_system.dart';
11 import 'package:analyzer/instrumentation/instrumentation.dart';
12 import 'package:analyzer/src/generated/bazel.dart';
13 import 'package:analyzer/src/generated/gn.dart';
14 import 'package:analyzer_plugin/channel/channel.dart';
15 import 'package:analyzer_plugin/protocol/protocol.dart';
16 import 'package:analyzer_plugin/protocol/protocol_generated.dart';
17 import 'package:analyzer_plugin/src/channel/isolate_channel.dart';
18 import 'package:analyzer_plugin/src/protocol/protocol_internal.dart';
19 import 'package:convert/convert.dart';
20 import 'package:crypto/crypto.dart';
21 import 'package:meta/meta.dart';
22 import 'package:path/path.dart' as path;
23
24 /**
25 * Information about a single plugin.
26 */
27 @visibleForTesting
28 class PluginInfo {
29 /**
30 * The path to the root directory of the definition of the plugin on disk (the
31 * directory containing the 'pubspec.yaml' file and the 'bin' directory).
32 */
33 final String path;
34
35 /**
36 * The path to the 'plugin.dart' file that will be executed in an isolate.
37 */
38 final String executionPath;
39
40 /**
41 * The path to the '.packages' file used to control the resolution of
42 * 'package:' URIs.
43 */
44 final String packagesPath;
45
46 /**
47 * The object used to manage the receiving and sending of notifications.
48 */
49 final NotificationManager notificationManager;
50
51 /**
52 * The instrumentation service that is being used by the analysis server.
53 */
54 final InstrumentationService instrumentationService;
55
56 /**
57 * The context roots that are currently using the results produced by the
58 * plugin.
59 */
60 Set<ContextRoot> contextRoots = new HashSet<ContextRoot>();
61
62 /**
63 * The current execution of the plugin, or `null` if the plugin is not
64 * currently being executed.
65 */
66 PluginSession currentSession;
67
68 /**
69 * Initialize the newly created information about a plugin.
70 */
71 PluginInfo(this.path, this.executionPath, this.packagesPath,
72 this.notificationManager, this.instrumentationService);
73
74 /**
75 * Add the given [contextRoot] to the set of context roots being analyzed by
76 * this plugin.
77 */
78 void addContextRoot(ContextRoot contextRoot) {
79 if (contextRoots.add(contextRoot)) {
80 _updatePluginRoots();
81 }
82 }
83
84 /**
85 * Remove the given [contextRoot] from the set of context roots being analyzed
86 * by this plugin.
87 */
88 void removeContextRoot(ContextRoot contextRoot) {
89 if (contextRoots.remove(contextRoot)) {
90 _updatePluginRoots();
91 }
92 }
93
94 /**
95 * Start a new isolate that is running the plugin. Return the state object
96 * used to interact with the plugin.
97 */
98 Future<PluginSession> start(String byteStorePath) async {
99 if (currentSession != null) {
100 throw new StateError('Cannot start a plugin that is already running.');
101 }
102 currentSession = new PluginSession(this);
103 await currentSession.start(byteStorePath);
104 return currentSession;
105 }
106
107 /**
108 * Request that the plugin shutdown.
109 */
110 Future<Null> stop() {
111 if (currentSession == null) {
112 throw new StateError('Cannot stop a plugin that is not running.');
113 }
114 Future<Null> doneFuture = currentSession.stop();
115 currentSession = null;
116 return doneFuture;
117 }
118
119 /**
120 * Update the context roots that the plugin should be analyzing.
121 */
122 void _updatePluginRoots() {
123 if (currentSession != null) {
124 AnalysisSetContextRootsParams params =
125 new AnalysisSetContextRootsParams(contextRoots.toList());
126 currentSession.sendRequest(params);
127 }
128 }
129 }
130
131 /**
132 * An object used to manage the currently running plugins.
133 */
134 class PluginManager {
135 /**
136 * The resource provider used to access the file system.
137 */
138 final ResourceProvider resourceProvider;
139
140 /**
141 * The absolute path of the directory containing the on-disk byte store, or
142 * `null` if there is no on-disk store.
143 */
144 final String byteStorePath;
145
146 /**
147 * The object used to manage the receiving and sending of notifications.
148 */
149 final NotificationManager notificationManager;
150
151 /**
152 * The instrumentation service that is being used by the analysis server.
153 */
154 final InstrumentationService instrumentationService;
155
156 /**
157 * A table mapping the paths of plugins to information about those plugins.
158 */
159 Map<String, PluginInfo> _pluginMap = <String, PluginInfo>{};
160
161 /**
162 * Initialize a newly created plugin manager. The notifications from the
163 * running plugins will be handled by the given [notificationManager].
164 */
165 PluginManager(this.resourceProvider, this.byteStorePath,
166 this.notificationManager, this.instrumentationService);
167
168 /**
169 * Add the plugin with the given [path] to the list of plugins that should be
170 * used when analyzing code for the given [contextRoot]. If the plugin had not
171 * yet been started, then it will be started by this method.
172 */
173 Future<Null> addPluginToContextRoot(
174 ContextRoot contextRoot, String path) async {
175 PluginInfo plugin = _pluginMap[path];
176 if (plugin == null) {
177 List<String> pluginPaths = _pathsFor(path);
178 plugin = new PluginInfo(path, pluginPaths[0], pluginPaths[1],
179 notificationManager, instrumentationService);
180 _pluginMap[path] = plugin;
181 if (pluginPaths[0] != null) {
182 PluginSession session = await plugin.start(byteStorePath);
183 session.onDone.then((_) {
184 _pluginMap.remove(path);
185 });
186 }
187 }
188 plugin.addContextRoot(contextRoot);
189 }
190
191 /**
192 * Broadcast a request built from the given [params] to all of the plugins
193 * that are currently associated with the given [contextRoot]. Return a list
194 * containing futures that will complete when each of the plugins have sent a
195 * response.
196 */
197 List<Future<Response>> broadcast(
198 ContextRoot contextRoot, RequestParams params) {
199 List<PluginInfo> plugins = pluginsForContextRoot(contextRoot);
200 return plugins
201 .map((PluginInfo plugin) => plugin.currentSession?.sendRequest(params))
202 .toList();
203 }
204
205 /**
206 * Return a list of all of the plugins that are currently associated with the
207 * given [contextRoot].
208 */
209 @visibleForTesting
210 List<PluginInfo> pluginsForContextRoot(ContextRoot contextRoot) {
211 List<PluginInfo> plugins = <PluginInfo>[];
212 for (PluginInfo plugin in _pluginMap.values) {
213 if (plugin.contextRoots.contains(contextRoot)) {
214 plugins.add(plugin);
215 }
216 }
217 return plugins;
218 }
219
220 /**
221 * The given [contextRoot] is no longer being analyzed.
222 */
223 void removedContextRoot(ContextRoot contextRoot) {
224 List<PluginInfo> plugins = _pluginMap.values.toList();
225 for (PluginInfo plugin in plugins) {
226 plugin.removeContextRoot(contextRoot);
227 if (plugin.contextRoots.isEmpty) {
228 _pluginMap.remove(plugin.path);
229 plugin.stop();
230 }
231 }
232 }
233
234 /**
235 * Stop all of the plugins that are currently running.
236 */
237 Future<List<Null>> stopAll() {
238 return Future.wait(_pluginMap.values.map((PluginInfo info) => info.stop()));
239 }
240
241 /**
242 * Return the execution path and .packages path associated with the plugin at
243 * the given [path], or `null` if there is a problem that prevents us from
244 * executing the plugin.
245 */
246 List<String> _pathsFor(String pluginPath) {
247 /**
248 * Return `true` if the plugin in the give [folder] needs to be copied to a
249 * temporary location so that 'pub' can be run to resolve dependencies. We
250 * need to run `pub` if the plugin contains a `pubspec.yaml` file and is not
251 * in a workspace.
252 */
253 bool needToCopy(Folder folder) {
254 File pubspecFile = folder.getChildAssumingFile('pubspec.yaml');
255 if (!pubspecFile.exists) {
256 return false;
257 }
258 return BazelWorkspace.find(resourceProvider, folder.path) == null &&
259 GnWorkspace.find(resourceProvider, folder.path) == null;
260 }
261
262 /**
263 * Compute the paths to be returned by the enclosing method given that the
264 * plugin should exist in the given [pluginFolder].
265 */
266 List<String> computePaths(Folder pluginFolder, {bool runPub: false}) {
267 File pluginFile = pluginFolder
268 .getChildAssumingFolder('bin')
269 .getChildAssumingFile('plugin.dart');
270 if (!pluginFile.exists) {
271 return null;
272 }
273 File packagesFile = pluginFolder.getChildAssumingFile('.packages');
274 if (!packagesFile.exists) {
275 if (runPub) {
276 // TODO(brianwilkerson) Run pub in the pluginFolder.
277 if (!packagesFile.exists) {
278 packagesFile = null;
279 }
280 }
281 packagesFile = null;
282 }
283 return <String>[pluginFile.path, packagesFile?.path];
284 }
285
286 Folder pluginFolder = resourceProvider.getFolder(pluginPath);
287 if (!needToCopy(pluginFolder)) {
288 return computePaths(pluginFolder);
289 }
290 //
291 // Copy the plugin directory to a unique subdirectory of the plugin
292 // manager's state location. The subdirectory's name is selected such that
293 // it will be invariant across sessions, reducing the number of times the
294 // plugin will need to be copied and pub will need to be run.
295 //
296 Folder stateFolder = resourceProvider.getStateLocation('.plugin_manager');
297 String stateName = _uniqueDirectoryName(pluginPath);
298 Folder parentFolder = stateFolder.getChildAssumingFolder(stateName);
299 if (parentFolder.exists) {
300 Folder executionFolder =
301 parentFolder.getChildAssumingFolder(pluginFolder.shortName);
302 return computePaths(executionFolder);
303 }
304 Folder executionFolder = pluginFolder.copyTo(parentFolder);
305 return computePaths(executionFolder, runPub: true);
306 }
307
308 /**
309 * Return a hex-encoded MD5 signature of the given file [path].
310 */
311 String _uniqueDirectoryName(String path) {
312 List<int> bytes = md5.convert(path.codeUnits).bytes;
313 return hex.encode(bytes);
314 }
315 }
316
317 /**
318 * Information about the execution a single plugin.
319 */
320 @visibleForTesting
321 class PluginSession {
322 /**
323 * The information about the plugin being executed.
324 */
325 final PluginInfo info;
326
327 /**
328 * The completer used to signal when the plugin has stopped.
329 */
330 Completer<Null> pluginStoppedCompleter = new Completer<Null>();
331
332 /**
333 * The channel used to communicate with the plugin.
334 */
335 ServerCommunicationChannel channel;
336
337 /**
338 * The index of the next request to be sent to the plugin.
339 */
340 int requestId = 0;
341
342 /**
343 * A table mapping the id's of requests to the functions used to handle the
344 * response to those requests.
345 */
346 Map<String, Completer<Response>> pendingRequests =
347 <String, Completer<Response>>{};
348
349 /**
350 * A boolean indicating whether the plugin is compatible with the version of
351 * the plugin API being used by this server.
352 */
353 bool isCompatible = true;
354
355 /**
356 * The contact information to include when reporting problems related to the
357 * plugin.
358 */
359 String contactInfo;
360
361 /**
362 * The glob patterns of files that the plugin is interested in knowing about.
363 */
364 List<String> interestingFiles;
365
366 /**
367 * The name to be used when reporting problems related to the plugin.
368 */
369 String name;
370
371 /**
372 * The version number to be used when reporting problems related to the
373 * plugin.
374 */
375 String version;
376
377 /**
378 * Initialize the newly created information about the execution of a plugin.
379 */
380 PluginSession(this.info);
381
382 /**
383 * Return the next request id, encoded as a string and increment the id so
384 * that a different result will be returned on each invocation.
385 */
386 String get nextRequestId => (requestId++).toString();
387
388 /**
389 * Return a future that will complete when the plugin has stopped.
390 */
391 Future<Null> get onDone => pluginStoppedCompleter.future;
392
393 /**
394 * Handle the given [notification].
395 */
396 void handleNotification(Notification notification) {
397 info.notificationManager.handlePluginNotification(info.path, notification);
398 }
399
400 /**
401 * Handle the fact that the plugin has stopped.
402 */
403 void handleOnDone() {
404 channel.close();
405 channel = null;
406 pluginStoppedCompleter.complete(null);
407 }
408
409 /**
410 * Handle the fact that an unhandled error has occurred in the plugin.
411 */
412 void handleOnError(List<String> errorPair) {
413 // TODO(brianwilkerson) Decide how we want to handle errors.
414 // String message = errorPair[0];
415 // String stackTrace = errorPair[1];
416 // print('PluginSession.handleOnError');
417 // print(' plugin = ${info.executionPath}');
418 // print(' $message');
419 // print(' ${new StackTrace.fromString(stackTrace)}');
420 // pluginStoppedCompleter.completeError(message, new StackTrace.fromString(st ackTrace));
421 }
422
423 /**
424 * Handle a [response] from the plugin by completing the future that was
425 * created when the request was sent.
426 */
427 void handleResponse(Response response) {
428 Completer<Response> completer = pendingRequests.remove(response.id);
429 if (completer != null) {
430 completer.complete(response);
431 }
432 }
433
434 /**
435 * Send a request, based on the given [parameters]. Return a future that will
436 * complete when a response is received.
437 */
438 Future<Response> sendRequest(RequestParams parameters) {
439 if (channel == null) {
440 throw new StateError(
441 'Cannot send a request to a plugin that has stopped.');
442 }
443 String id = nextRequestId;
444 Completer<Response> completer = new Completer();
445 pendingRequests[id] = completer;
446 channel.sendRequest(parameters.toRequest(id));
447 return completer.future;
448 }
449
450 /**
451 * Start a new isolate that is running this plugin. The plugin will be sent
452 * the given [byteStorePath]. Return `true` if the plugin is compatible and
453 * running.
454 */
455 Future<bool> start(String byteStorePath) async {
456 if (channel != null) {
457 throw new StateError('Cannot start a plugin that is already running.');
458 }
459 if (byteStorePath == null || byteStorePath.isEmpty) {
460 throw new StateError('Missing byte store path');
461 }
462 if (!isCompatible) {
463 return false;
464 }
465 channel = new ServerIsolateChannel(
466 new Uri.file(info.executionPath, windows: Platform.isWindows),
467 new Uri.file(info.packagesPath, windows: Platform.isWindows),
468 info.instrumentationService);
469 await channel.listen(handleResponse, handleNotification,
470 onDone: handleOnDone, onError: handleOnError);
471 Response response = await sendRequest(
472 new PluginVersionCheckParams(byteStorePath ?? '', '1.0.0-alpha.0'));
473 PluginVersionCheckResult result =
474 new PluginVersionCheckResult.fromResponse(response);
475 isCompatible = result.isCompatible;
476 contactInfo = result.contactInfo;
477 interestingFiles = result.interestingFiles;
478 name = result.name;
479 version = result.version;
480 if (!isCompatible) {
481 sendRequest(new PluginShutdownParams());
482 return false;
483 }
484 return true;
485 }
486
487 /**
488 * Request that the plugin shutdown.
489 */
490 Future<Null> stop() {
491 if (channel == null) {
492 throw new StateError('Cannot stop a plugin that is not running.');
493 }
494 // TODO(brianwilkerson) Ensure that the isolate is killed if it does not
495 // terminate normally.
496 sendRequest(new PluginShutdownParams());
497 return pluginStoppedCompleter.future;
498 }
499 }
OLDNEW
« no previous file with comments | « no previous file | pkg/analysis_server/test/src/plugin/plugin_manager_test.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698