| Index: pkg/analysis_server/lib/src/plugin/plugin_manager.dart
|
| diff --git a/pkg/analysis_server/lib/src/plugin/plugin_manager.dart b/pkg/analysis_server/lib/src/plugin/plugin_manager.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..2d9f8ca1967b8d98f289bb2e1035f1a5994cbdda
|
| --- /dev/null
|
| +++ b/pkg/analysis_server/lib/src/plugin/plugin_manager.dart
|
| @@ -0,0 +1,499 @@
|
| +// 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:collection';
|
| +import 'dart:io' show Platform;
|
| +
|
| +import 'package:analysis_server/src/plugin/notification_manager.dart';
|
| +import 'package:analyzer/file_system/file_system.dart';
|
| +import 'package:analyzer/instrumentation/instrumentation.dart';
|
| +import 'package:analyzer/src/generated/bazel.dart';
|
| +import 'package:analyzer/src/generated/gn.dart';
|
| +import 'package:analyzer_plugin/channel/channel.dart';
|
| +import 'package:analyzer_plugin/protocol/protocol.dart';
|
| +import 'package:analyzer_plugin/protocol/protocol_generated.dart';
|
| +import 'package:analyzer_plugin/src/channel/isolate_channel.dart';
|
| +import 'package:analyzer_plugin/src/protocol/protocol_internal.dart';
|
| +import 'package:convert/convert.dart';
|
| +import 'package:crypto/crypto.dart';
|
| +import 'package:meta/meta.dart';
|
| +import 'package:path/path.dart' as path;
|
| +
|
| +/**
|
| + * Information about a single plugin.
|
| + */
|
| +@visibleForTesting
|
| +class PluginInfo {
|
| + /**
|
| + * The path to the root directory of the definition of the plugin on disk (the
|
| + * directory containing the 'pubspec.yaml' file and the 'bin' directory).
|
| + */
|
| + final String path;
|
| +
|
| + /**
|
| + * The path to the 'plugin.dart' file that will be executed in an isolate.
|
| + */
|
| + final String executionPath;
|
| +
|
| + /**
|
| + * The path to the '.packages' file used to control the resolution of
|
| + * 'package:' URIs.
|
| + */
|
| + final String packagesPath;
|
| +
|
| + /**
|
| + * The object used to manage the receiving and sending of notifications.
|
| + */
|
| + final NotificationManager notificationManager;
|
| +
|
| + /**
|
| + * The instrumentation service that is being used by the analysis server.
|
| + */
|
| + final InstrumentationService instrumentationService;
|
| +
|
| + /**
|
| + * The context roots that are currently using the results produced by the
|
| + * plugin.
|
| + */
|
| + Set<ContextRoot> contextRoots = new HashSet<ContextRoot>();
|
| +
|
| + /**
|
| + * The current execution of the plugin, or `null` if the plugin is not
|
| + * currently being executed.
|
| + */
|
| + PluginSession currentSession;
|
| +
|
| + /**
|
| + * Initialize the newly created information about a plugin.
|
| + */
|
| + PluginInfo(this.path, this.executionPath, this.packagesPath,
|
| + this.notificationManager, this.instrumentationService);
|
| +
|
| + /**
|
| + * Add the given [contextRoot] to the set of context roots being analyzed by
|
| + * this plugin.
|
| + */
|
| + void addContextRoot(ContextRoot contextRoot) {
|
| + if (contextRoots.add(contextRoot)) {
|
| + _updatePluginRoots();
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Remove the given [contextRoot] from the set of context roots being analyzed
|
| + * by this plugin.
|
| + */
|
| + void removeContextRoot(ContextRoot contextRoot) {
|
| + if (contextRoots.remove(contextRoot)) {
|
| + _updatePluginRoots();
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Start a new isolate that is running the plugin. Return the state object
|
| + * used to interact with the plugin.
|
| + */
|
| + Future<PluginSession> start(String byteStorePath) async {
|
| + if (currentSession != null) {
|
| + throw new StateError('Cannot start a plugin that is already running.');
|
| + }
|
| + currentSession = new PluginSession(this);
|
| + await currentSession.start(byteStorePath);
|
| + return currentSession;
|
| + }
|
| +
|
| + /**
|
| + * Request that the plugin shutdown.
|
| + */
|
| + Future<Null> stop() {
|
| + if (currentSession == null) {
|
| + throw new StateError('Cannot stop a plugin that is not running.');
|
| + }
|
| + Future<Null> doneFuture = currentSession.stop();
|
| + currentSession = null;
|
| + return doneFuture;
|
| + }
|
| +
|
| + /**
|
| + * Update the context roots that the plugin should be analyzing.
|
| + */
|
| + void _updatePluginRoots() {
|
| + if (currentSession != null) {
|
| + AnalysisSetContextRootsParams params =
|
| + new AnalysisSetContextRootsParams(contextRoots.toList());
|
| + currentSession.sendRequest(params);
|
| + }
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * An object used to manage the currently running plugins.
|
| + */
|
| +class PluginManager {
|
| + /**
|
| + * The resource provider used to access the file system.
|
| + */
|
| + final ResourceProvider resourceProvider;
|
| +
|
| + /**
|
| + * The absolute path of the directory containing the on-disk byte store, or
|
| + * `null` if there is no on-disk store.
|
| + */
|
| + final String byteStorePath;
|
| +
|
| + /**
|
| + * The object used to manage the receiving and sending of notifications.
|
| + */
|
| + final NotificationManager notificationManager;
|
| +
|
| + /**
|
| + * The instrumentation service that is being used by the analysis server.
|
| + */
|
| + final InstrumentationService instrumentationService;
|
| +
|
| + /**
|
| + * A table mapping the paths of plugins to information about those plugins.
|
| + */
|
| + Map<String, PluginInfo> _pluginMap = <String, PluginInfo>{};
|
| +
|
| + /**
|
| + * Initialize a newly created plugin manager. The notifications from the
|
| + * running plugins will be handled by the given [notificationManager].
|
| + */
|
| + PluginManager(this.resourceProvider, this.byteStorePath,
|
| + this.notificationManager, this.instrumentationService);
|
| +
|
| + /**
|
| + * Add the plugin with the given [path] to the list of plugins that should be
|
| + * used when analyzing code for the given [contextRoot]. If the plugin had not
|
| + * yet been started, then it will be started by this method.
|
| + */
|
| + Future<Null> addPluginToContextRoot(
|
| + ContextRoot contextRoot, String path) async {
|
| + PluginInfo plugin = _pluginMap[path];
|
| + if (plugin == null) {
|
| + List<String> pluginPaths = _pathsFor(path);
|
| + plugin = new PluginInfo(path, pluginPaths[0], pluginPaths[1],
|
| + notificationManager, instrumentationService);
|
| + _pluginMap[path] = plugin;
|
| + if (pluginPaths[0] != null) {
|
| + PluginSession session = await plugin.start(byteStorePath);
|
| + session.onDone.then((_) {
|
| + _pluginMap.remove(path);
|
| + });
|
| + }
|
| + }
|
| + plugin.addContextRoot(contextRoot);
|
| + }
|
| +
|
| + /**
|
| + * Broadcast a request built from the given [params] to all of the plugins
|
| + * that are currently associated with the given [contextRoot]. Return a list
|
| + * containing futures that will complete when each of the plugins have sent a
|
| + * response.
|
| + */
|
| + List<Future<Response>> broadcast(
|
| + ContextRoot contextRoot, RequestParams params) {
|
| + List<PluginInfo> plugins = pluginsForContextRoot(contextRoot);
|
| + return plugins
|
| + .map((PluginInfo plugin) => plugin.currentSession?.sendRequest(params))
|
| + .toList();
|
| + }
|
| +
|
| + /**
|
| + * Return a list of all of the plugins that are currently associated with the
|
| + * given [contextRoot].
|
| + */
|
| + @visibleForTesting
|
| + List<PluginInfo> pluginsForContextRoot(ContextRoot contextRoot) {
|
| + List<PluginInfo> plugins = <PluginInfo>[];
|
| + for (PluginInfo plugin in _pluginMap.values) {
|
| + if (plugin.contextRoots.contains(contextRoot)) {
|
| + plugins.add(plugin);
|
| + }
|
| + }
|
| + return plugins;
|
| + }
|
| +
|
| + /**
|
| + * The given [contextRoot] is no longer being analyzed.
|
| + */
|
| + void removedContextRoot(ContextRoot contextRoot) {
|
| + List<PluginInfo> plugins = _pluginMap.values.toList();
|
| + for (PluginInfo plugin in plugins) {
|
| + plugin.removeContextRoot(contextRoot);
|
| + if (plugin.contextRoots.isEmpty) {
|
| + _pluginMap.remove(plugin.path);
|
| + plugin.stop();
|
| + }
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Stop all of the plugins that are currently running.
|
| + */
|
| + Future<List<Null>> stopAll() {
|
| + return Future.wait(_pluginMap.values.map((PluginInfo info) => info.stop()));
|
| + }
|
| +
|
| + /**
|
| + * Return the execution path and .packages path associated with the plugin at
|
| + * the given [path], or `null` if there is a problem that prevents us from
|
| + * executing the plugin.
|
| + */
|
| + List<String> _pathsFor(String pluginPath) {
|
| + /**
|
| + * Return `true` if the plugin in the give [folder] needs to be copied to a
|
| + * temporary location so that 'pub' can be run to resolve dependencies. We
|
| + * need to run `pub` if the plugin contains a `pubspec.yaml` file and is not
|
| + * in a workspace.
|
| + */
|
| + bool needToCopy(Folder folder) {
|
| + File pubspecFile = folder.getChildAssumingFile('pubspec.yaml');
|
| + if (!pubspecFile.exists) {
|
| + return false;
|
| + }
|
| + return BazelWorkspace.find(resourceProvider, folder.path) == null &&
|
| + GnWorkspace.find(resourceProvider, folder.path) == null;
|
| + }
|
| +
|
| + /**
|
| + * Compute the paths to be returned by the enclosing method given that the
|
| + * plugin should exist in the given [pluginFolder].
|
| + */
|
| + List<String> computePaths(Folder pluginFolder, {bool runPub: false}) {
|
| + File pluginFile = pluginFolder
|
| + .getChildAssumingFolder('bin')
|
| + .getChildAssumingFile('plugin.dart');
|
| + if (!pluginFile.exists) {
|
| + return null;
|
| + }
|
| + File packagesFile = pluginFolder.getChildAssumingFile('.packages');
|
| + if (!packagesFile.exists) {
|
| + if (runPub) {
|
| + // TODO(brianwilkerson) Run pub in the pluginFolder.
|
| + if (!packagesFile.exists) {
|
| + packagesFile = null;
|
| + }
|
| + }
|
| + packagesFile = null;
|
| + }
|
| + return <String>[pluginFile.path, packagesFile?.path];
|
| + }
|
| +
|
| + Folder pluginFolder = resourceProvider.getFolder(pluginPath);
|
| + if (!needToCopy(pluginFolder)) {
|
| + return computePaths(pluginFolder);
|
| + }
|
| + //
|
| + // Copy the plugin directory to a unique subdirectory of the plugin
|
| + // manager's state location. The subdirectory's name is selected such that
|
| + // it will be invariant across sessions, reducing the number of times the
|
| + // plugin will need to be copied and pub will need to be run.
|
| + //
|
| + Folder stateFolder = resourceProvider.getStateLocation('.plugin_manager');
|
| + String stateName = _uniqueDirectoryName(pluginPath);
|
| + Folder parentFolder = stateFolder.getChildAssumingFolder(stateName);
|
| + if (parentFolder.exists) {
|
| + Folder executionFolder =
|
| + parentFolder.getChildAssumingFolder(pluginFolder.shortName);
|
| + return computePaths(executionFolder);
|
| + }
|
| + Folder executionFolder = pluginFolder.copyTo(parentFolder);
|
| + return computePaths(executionFolder, runPub: true);
|
| + }
|
| +
|
| + /**
|
| + * Return a hex-encoded MD5 signature of the given file [path].
|
| + */
|
| + String _uniqueDirectoryName(String path) {
|
| + List<int> bytes = md5.convert(path.codeUnits).bytes;
|
| + return hex.encode(bytes);
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * Information about the execution a single plugin.
|
| + */
|
| +@visibleForTesting
|
| +class PluginSession {
|
| + /**
|
| + * The information about the plugin being executed.
|
| + */
|
| + final PluginInfo info;
|
| +
|
| + /**
|
| + * The completer used to signal when the plugin has stopped.
|
| + */
|
| + Completer<Null> pluginStoppedCompleter = new Completer<Null>();
|
| +
|
| + /**
|
| + * The channel used to communicate with the plugin.
|
| + */
|
| + ServerCommunicationChannel channel;
|
| +
|
| + /**
|
| + * The index of the next request to be sent to the plugin.
|
| + */
|
| + int requestId = 0;
|
| +
|
| + /**
|
| + * A table mapping the id's of requests to the functions used to handle the
|
| + * response to those requests.
|
| + */
|
| + Map<String, Completer<Response>> pendingRequests =
|
| + <String, Completer<Response>>{};
|
| +
|
| + /**
|
| + * A boolean indicating whether the plugin is compatible with the version of
|
| + * the plugin API being used by this server.
|
| + */
|
| + bool isCompatible = true;
|
| +
|
| + /**
|
| + * The contact information to include when reporting problems related to the
|
| + * plugin.
|
| + */
|
| + String contactInfo;
|
| +
|
| + /**
|
| + * The glob patterns of files that the plugin is interested in knowing about.
|
| + */
|
| + List<String> interestingFiles;
|
| +
|
| + /**
|
| + * The name to be used when reporting problems related to the plugin.
|
| + */
|
| + String name;
|
| +
|
| + /**
|
| + * The version number to be used when reporting problems related to the
|
| + * plugin.
|
| + */
|
| + String version;
|
| +
|
| + /**
|
| + * Initialize the newly created information about the execution of a plugin.
|
| + */
|
| + PluginSession(this.info);
|
| +
|
| + /**
|
| + * Return the next request id, encoded as a string and increment the id so
|
| + * that a different result will be returned on each invocation.
|
| + */
|
| + String get nextRequestId => (requestId++).toString();
|
| +
|
| + /**
|
| + * Return a future that will complete when the plugin has stopped.
|
| + */
|
| + Future<Null> get onDone => pluginStoppedCompleter.future;
|
| +
|
| + /**
|
| + * Handle the given [notification].
|
| + */
|
| + void handleNotification(Notification notification) {
|
| + info.notificationManager.handlePluginNotification(info.path, notification);
|
| + }
|
| +
|
| + /**
|
| + * Handle the fact that the plugin has stopped.
|
| + */
|
| + void handleOnDone() {
|
| + channel.close();
|
| + channel = null;
|
| + pluginStoppedCompleter.complete(null);
|
| + }
|
| +
|
| + /**
|
| + * Handle the fact that an unhandled error has occurred in the plugin.
|
| + */
|
| + void handleOnError(List<String> errorPair) {
|
| + // TODO(brianwilkerson) Decide how we want to handle errors.
|
| +// String message = errorPair[0];
|
| +// String stackTrace = errorPair[1];
|
| +// print('PluginSession.handleOnError');
|
| +// print(' plugin = ${info.executionPath}');
|
| +// print(' $message');
|
| +// print(' ${new StackTrace.fromString(stackTrace)}');
|
| +// pluginStoppedCompleter.completeError(message, new StackTrace.fromString(stackTrace));
|
| + }
|
| +
|
| + /**
|
| + * Handle a [response] from the plugin by completing the future that was
|
| + * created when the request was sent.
|
| + */
|
| + void handleResponse(Response response) {
|
| + Completer<Response> completer = pendingRequests.remove(response.id);
|
| + if (completer != null) {
|
| + completer.complete(response);
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Send a request, based on the given [parameters]. Return a future that will
|
| + * complete when a response is received.
|
| + */
|
| + Future<Response> sendRequest(RequestParams parameters) {
|
| + if (channel == null) {
|
| + throw new StateError(
|
| + 'Cannot send a request to a plugin that has stopped.');
|
| + }
|
| + String id = nextRequestId;
|
| + Completer<Response> completer = new Completer();
|
| + pendingRequests[id] = completer;
|
| + channel.sendRequest(parameters.toRequest(id));
|
| + return completer.future;
|
| + }
|
| +
|
| + /**
|
| + * Start a new isolate that is running this plugin. The plugin will be sent
|
| + * the given [byteStorePath]. Return `true` if the plugin is compatible and
|
| + * running.
|
| + */
|
| + Future<bool> start(String byteStorePath) async {
|
| + if (channel != null) {
|
| + throw new StateError('Cannot start a plugin that is already running.');
|
| + }
|
| + if (byteStorePath == null || byteStorePath.isEmpty) {
|
| + throw new StateError('Missing byte store path');
|
| + }
|
| + if (!isCompatible) {
|
| + return false;
|
| + }
|
| + channel = new ServerIsolateChannel(
|
| + new Uri.file(info.executionPath, windows: Platform.isWindows),
|
| + new Uri.file(info.packagesPath, windows: Platform.isWindows),
|
| + info.instrumentationService);
|
| + await channel.listen(handleResponse, handleNotification,
|
| + onDone: handleOnDone, onError: handleOnError);
|
| + Response response = await sendRequest(
|
| + new PluginVersionCheckParams(byteStorePath ?? '', '1.0.0-alpha.0'));
|
| + PluginVersionCheckResult result =
|
| + new PluginVersionCheckResult.fromResponse(response);
|
| + isCompatible = result.isCompatible;
|
| + contactInfo = result.contactInfo;
|
| + interestingFiles = result.interestingFiles;
|
| + name = result.name;
|
| + version = result.version;
|
| + if (!isCompatible) {
|
| + sendRequest(new PluginShutdownParams());
|
| + return false;
|
| + }
|
| + return true;
|
| + }
|
| +
|
| + /**
|
| + * Request that the plugin shutdown.
|
| + */
|
| + Future<Null> stop() {
|
| + if (channel == null) {
|
| + throw new StateError('Cannot stop a plugin that is not running.');
|
| + }
|
| + // TODO(brianwilkerson) Ensure that the isolate is killed if it does not
|
| + // terminate normally.
|
| + sendRequest(new PluginShutdownParams());
|
| + return pluginStoppedCompleter.future;
|
| + }
|
| +}
|
|
|