1043 lines
33 KiB
Dart
1043 lines
33 KiB
Dart
// 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:convert';
|
|
import 'dart:io' show Platform, Process, ProcessResult;
|
|
|
|
import 'package:analysis_server/src/plugin/notification_manager.dart';
|
|
import 'package:analyzer/context/context_root.dart' as analyzer;
|
|
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/src/generated/source.dart';
|
|
import 'package:analyzer/src/generated/workspace.dart';
|
|
import 'package:analyzer/src/util/glob.dart';
|
|
import 'package:analyzer_plugin/channel/channel.dart';
|
|
import 'package:analyzer_plugin/protocol/protocol.dart';
|
|
import 'package:analyzer_plugin/protocol/protocol_common.dart';
|
|
import 'package:analyzer_plugin/protocol/protocol_constants.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;
|
|
import 'package:watcher/watcher.dart' as watcher;
|
|
import 'package:yaml/yaml.dart';
|
|
|
|
/**
|
|
* Information about a plugin that is built-in.
|
|
*/
|
|
class BuiltInPluginInfo extends PluginInfo {
|
|
/**
|
|
* The entry point function that will be executed in the plugin's isolate.
|
|
*/
|
|
final EntryPoint entryPoint;
|
|
|
|
@override
|
|
final String pluginId;
|
|
|
|
/**
|
|
* Initialize a newly created built-in plugin.
|
|
*/
|
|
BuiltInPluginInfo(
|
|
this.entryPoint,
|
|
this.pluginId,
|
|
NotificationManager notificationManager,
|
|
InstrumentationService instrumentationService)
|
|
: super(notificationManager, instrumentationService);
|
|
|
|
@override
|
|
ServerCommunicationChannel _createChannel() {
|
|
return new ServerIsolateChannel.builtIn(
|
|
entryPoint, pluginId, instrumentationService);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Information about a plugin that was discovered.
|
|
*/
|
|
class DiscoveredPluginInfo extends 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;
|
|
|
|
/**
|
|
* Initialize the newly created information about a plugin.
|
|
*/
|
|
DiscoveredPluginInfo(
|
|
this.path,
|
|
this.executionPath,
|
|
this.packagesPath,
|
|
NotificationManager notificationManager,
|
|
InstrumentationService instrumentationService)
|
|
: super(notificationManager, instrumentationService);
|
|
|
|
@override
|
|
String get pluginId => path;
|
|
|
|
@override
|
|
ServerCommunicationChannel _createChannel() {
|
|
return new ServerIsolateChannel.discovered(
|
|
new Uri.file(executionPath, windows: Platform.isWindows),
|
|
new Uri.file(packagesPath, windows: Platform.isWindows),
|
|
instrumentationService);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Information about a single plugin.
|
|
*/
|
|
abstract class PluginInfo {
|
|
/**
|
|
* 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<analyzer.ContextRoot> contextRoots = new HashSet<analyzer.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.notificationManager, this.instrumentationService);
|
|
|
|
/**
|
|
* Return the data known about this plugin.
|
|
*/
|
|
PluginData get data =>
|
|
new PluginData(pluginId, currentSession?.name, currentSession?.version);
|
|
|
|
/**
|
|
* Return the id of this plugin, used to identify the plugin to users.
|
|
*/
|
|
String get pluginId;
|
|
|
|
/**
|
|
* Add the given [contextRoot] to the set of context roots being analyzed by
|
|
* this plugin.
|
|
*/
|
|
void addContextRoot(analyzer.ContextRoot contextRoot) {
|
|
if (contextRoots.add(contextRoot)) {
|
|
_updatePluginRoots();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add the given context [roots] to the set of context roots being analyzed by
|
|
* this plugin.
|
|
*/
|
|
void addContextRoots(Iterable<analyzer.ContextRoot> roots) {
|
|
bool changed = false;
|
|
for (analyzer.ContextRoot contextRoot in roots) {
|
|
if (contextRoots.add(contextRoot)) {
|
|
changed = true;
|
|
}
|
|
}
|
|
if (changed) {
|
|
_updatePluginRoots();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return `true` if at least one of the context roots being analyzed contains
|
|
* the file with the given [filePath].
|
|
*/
|
|
bool isAnalyzing(String filePath) {
|
|
for (var contextRoot in contextRoots) {
|
|
if (contextRoot.containsFile(filePath)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Remove the given [contextRoot] from the set of context roots being analyzed
|
|
* by this plugin.
|
|
*/
|
|
void removeContextRoot(analyzer.ContextRoot contextRoot) {
|
|
if (contextRoots.remove(contextRoot)) {
|
|
_updatePluginRoots();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If the plugin is currently running, send a request based on the given
|
|
* [params] to the plugin. If the plugin is not running, the request will
|
|
* silently be dropped.
|
|
*/
|
|
void sendRequest(RequestParams params) {
|
|
currentSession?.sendRequest(params);
|
|
}
|
|
|
|
/**
|
|
* Start a new isolate that is running the plugin. Return the state object
|
|
* used to interact with the plugin, or `null` if the plugin could not be run.
|
|
*/
|
|
Future<PluginSession> start(String byteStorePath, String sdkPath) async {
|
|
if (currentSession != null) {
|
|
throw new StateError('Cannot start a plugin that is already running.');
|
|
}
|
|
currentSession = new PluginSession(this);
|
|
bool isRunning = await currentSession.start(byteStorePath, sdkPath);
|
|
if (!isRunning) {
|
|
currentSession = null;
|
|
}
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Create the channel used to communicate with the server.
|
|
*/
|
|
ServerCommunicationChannel _createChannel();
|
|
|
|
/**
|
|
* Update the context roots that the plugin should be analyzing.
|
|
*/
|
|
void _updatePluginRoots() {
|
|
if (currentSession != null) {
|
|
AnalysisSetContextRootsParams params = new AnalysisSetContextRootsParams(
|
|
contextRoots
|
|
.map((analyzer.ContextRoot contextRoot) => new ContextRoot(
|
|
contextRoot.root, contextRoot.exclude,
|
|
optionsFile: contextRoot.optionsFilePath))
|
|
.toList());
|
|
currentSession.sendRequest(params);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An object used to manage the currently running plugins.
|
|
*/
|
|
class PluginManager {
|
|
/**
|
|
* A table, keyed by both a plugin and a request method, to a list of the
|
|
* times that it took the plugin to return a response to requests with the
|
|
* method.
|
|
*/
|
|
static Map<PluginInfo, Map<String, List<int>>> pluginResponseTimes =
|
|
<PluginInfo, Map<String, List<int>>>{};
|
|
|
|
/**
|
|
* 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 absolute path of the directory containing the SDK.
|
|
*/
|
|
final String sdkPath;
|
|
|
|
/**
|
|
* 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 list of globs used to match plugin paths that have been whitelisted.
|
|
*/
|
|
List<Glob> _whitelistGlobs;
|
|
|
|
/**
|
|
* A table mapping the paths of plugins to information about those plugins.
|
|
*/
|
|
Map<String, PluginInfo> _pluginMap = <String, PluginInfo>{};
|
|
|
|
/**
|
|
* The parameters for the last 'analysis.setPriorityFiles' request that was
|
|
* received from the client. Because plugins are lazily discovered, this needs
|
|
* to be retained so that it can be sent after a plugin has been started.
|
|
*/
|
|
AnalysisSetPriorityFilesParams _analysisSetPriorityFilesParams;
|
|
|
|
/**
|
|
* The parameters for the last 'analysis.setSubscriptions' request that was
|
|
* received from the client. Because plugins are lazily discovered, this needs
|
|
* to be retained so that it can be sent after a plugin has been started.
|
|
*/
|
|
AnalysisSetSubscriptionsParams _analysisSetSubscriptionsParams;
|
|
|
|
/**
|
|
* The current state of content overlays. Because plugins are lazily
|
|
* discovered, the state needs to be retained so that it can be sent after a
|
|
* plugin has been started.
|
|
*/
|
|
Map<String, dynamic> _overlayState = <String, dynamic>{};
|
|
|
|
/**
|
|
* 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.sdkPath,
|
|
this.notificationManager, this.instrumentationService) {
|
|
// TODO(brianwilkerson) Figure out the right list of plugin paths.
|
|
_whitelistGlobs = <Glob>[
|
|
new Glob(resourceProvider.pathContext.separator,
|
|
'**/angular_analyzer_plugin/tools/analyzer_plugin'),
|
|
new Glob(resourceProvider.pathContext.separator,
|
|
'**angular/tools/analyzer_plugin')
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Return a list of all of the plugins that are currently known.
|
|
*/
|
|
@visibleForTesting
|
|
List<PluginInfo> get plugins => _pluginMap.values.toList();
|
|
|
|
/**
|
|
* 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(
|
|
analyzer.ContextRoot contextRoot, String path) async {
|
|
if (!_isWhitelisted(path)) {
|
|
return;
|
|
}
|
|
PluginInfo plugin = _pluginMap[path];
|
|
bool isNew = plugin == null;
|
|
if (isNew) {
|
|
List<String> pluginPaths = pathsFor(path);
|
|
if (pluginPaths == null) {
|
|
return;
|
|
}
|
|
plugin = new DiscoveredPluginInfo(path, pluginPaths[0], pluginPaths[1],
|
|
notificationManager, instrumentationService);
|
|
_pluginMap[path] = plugin;
|
|
if (pluginPaths[0] != null) {
|
|
PluginSession session = await plugin.start(byteStorePath, sdkPath);
|
|
session?.onDone?.then((_) {
|
|
_pluginMap.remove(path);
|
|
});
|
|
}
|
|
}
|
|
plugin.addContextRoot(contextRoot);
|
|
if (isNew) {
|
|
if (_analysisSetSubscriptionsParams != null) {
|
|
plugin.sendRequest(_analysisSetSubscriptionsParams);
|
|
}
|
|
if (_overlayState.isNotEmpty) {
|
|
plugin.sendRequest(new AnalysisUpdateContentParams(_overlayState));
|
|
}
|
|
if (_analysisSetPriorityFilesParams != null) {
|
|
plugin.sendRequest(_analysisSetPriorityFilesParams);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
Map<PluginInfo, Future<Response>> broadcastRequest(RequestParams params,
|
|
{analyzer.ContextRoot contextRoot}) {
|
|
List<PluginInfo> plugins = pluginsForContextRoot(contextRoot);
|
|
Map<PluginInfo, Future<Response>> responseMap =
|
|
<PluginInfo, Future<Response>>{};
|
|
for (PluginInfo plugin in plugins) {
|
|
responseMap[plugin] = plugin.currentSession?.sendRequest(params);
|
|
}
|
|
return responseMap;
|
|
}
|
|
|
|
/**
|
|
* Broadcast the given [watchEvent] to all of the plugins that are analyzing
|
|
* in contexts containing the file associated with the event. Return a list
|
|
* containing futures that will complete when each of the plugins have sent a
|
|
* response.
|
|
*/
|
|
Future<List<Future<Response>>> broadcastWatchEvent(
|
|
watcher.WatchEvent watchEvent) async {
|
|
String filePath = watchEvent.path;
|
|
|
|
/**
|
|
* Return `true` if the given glob [pattern] matches the file being watched.
|
|
*/
|
|
bool matches(String pattern) =>
|
|
new Glob(resourceProvider.pathContext.separator, pattern)
|
|
.matches(filePath);
|
|
|
|
WatchEvent event = null;
|
|
List<Future<Response>> responses = <Future<Response>>[];
|
|
for (PluginInfo plugin in _pluginMap.values) {
|
|
PluginSession session = plugin.currentSession;
|
|
if (session != null &&
|
|
plugin.isAnalyzing(filePath) &&
|
|
session.interestingFiles.any(matches)) {
|
|
event ??= _convertWatchEvent(watchEvent);
|
|
AnalysisHandleWatchEventsParams params =
|
|
new AnalysisHandleWatchEventsParams([event]);
|
|
responses.add(session.sendRequest(params));
|
|
}
|
|
}
|
|
return responses;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
@visibleForTesting
|
|
List<String> pathsFor(String pluginPath) {
|
|
Folder pluginFolder = resourceProvider.getFolder(pluginPath);
|
|
File pubspecFile = pluginFolder.getChildAssumingFile('pubspec.yaml');
|
|
if (!pubspecFile.exists) {
|
|
// If there's no pubspec file, then we don't need to copy the package
|
|
// because we won't be running pub.
|
|
return _computePaths(pluginFolder);
|
|
}
|
|
Workspace workspace =
|
|
BazelWorkspace.find(resourceProvider, pluginFolder.path) ??
|
|
GnWorkspace.find(resourceProvider, pluginFolder.path);
|
|
if (workspace != null) {
|
|
// Similarly, we won't be running pub if we're in a workspace because
|
|
// there is exactly one version of each package.
|
|
return _computePaths(pluginFolder, workspace: workspace);
|
|
}
|
|
//
|
|
// 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 list of all of the plugins that are currently associated with the
|
|
* given [contextRoot].
|
|
*/
|
|
@visibleForTesting
|
|
List<PluginInfo> pluginsForContextRoot(analyzer.ContextRoot contextRoot) {
|
|
if (contextRoot == null) {
|
|
return _pluginMap.values.toList();
|
|
}
|
|
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(analyzer.ContextRoot contextRoot) {
|
|
List<PluginInfo> plugins = _pluginMap.values.toList();
|
|
for (PluginInfo plugin in plugins) {
|
|
plugin.removeContextRoot(contextRoot);
|
|
if (plugin is DiscoveredPluginInfo && plugin.contextRoots.isEmpty) {
|
|
_pluginMap.remove(plugin.path);
|
|
plugin.stop();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Restart all currently running plugins.
|
|
*/
|
|
Future<Null> restartPlugins() async {
|
|
for (PluginInfo plugin in _pluginMap.values.toList()) {
|
|
if (plugin.currentSession != null) {
|
|
//
|
|
// Capture needed state.
|
|
//
|
|
Set<analyzer.ContextRoot> contextRoots = plugin.contextRoots;
|
|
String path = plugin.pluginId;
|
|
//
|
|
// Stop the plugin.
|
|
//
|
|
await plugin.stop();
|
|
//
|
|
// Restart the plugin.
|
|
//
|
|
_pluginMap[path] = plugin;
|
|
PluginSession session = await plugin.start(byteStorePath, sdkPath);
|
|
session?.onDone?.then((_) {
|
|
_pluginMap.remove(path);
|
|
});
|
|
//
|
|
// Re-initialize the plugin.
|
|
//
|
|
plugin.addContextRoots(contextRoots);
|
|
if (_analysisSetSubscriptionsParams != null) {
|
|
plugin.sendRequest(_analysisSetSubscriptionsParams);
|
|
}
|
|
if (_overlayState.isNotEmpty) {
|
|
plugin.sendRequest(new AnalysisUpdateContentParams(_overlayState));
|
|
}
|
|
if (_analysisSetPriorityFilesParams != null) {
|
|
plugin.sendRequest(_analysisSetPriorityFilesParams);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send a request based on the given [params] to existing plugins to set the
|
|
* priority files to those specified by the [params]. As a side-effect, record
|
|
* the parameters so that they can be sent to any newly started plugins.
|
|
*/
|
|
void setAnalysisSetPriorityFilesParams(
|
|
AnalysisSetPriorityFilesParams params) {
|
|
for (PluginInfo plugin in _pluginMap.values) {
|
|
plugin.sendRequest(params);
|
|
}
|
|
_analysisSetPriorityFilesParams = params;
|
|
}
|
|
|
|
/**
|
|
* Send a request based on the given [params] to existing plugins to set the
|
|
* subscriptions to those specified by the [params]. As a side-effect, record
|
|
* the parameters so that they can be sent to any newly started plugins.
|
|
*/
|
|
void setAnalysisSetSubscriptionsParams(
|
|
AnalysisSetSubscriptionsParams params) {
|
|
for (PluginInfo plugin in _pluginMap.values) {
|
|
plugin.sendRequest(params);
|
|
}
|
|
_analysisSetSubscriptionsParams = params;
|
|
}
|
|
|
|
/**
|
|
* Send a request based on the given [params] to existing plugins to set the
|
|
* content overlays to those specified by the [params]. As a side-effect,
|
|
* update the overlay state so that it can be sent to any newly started
|
|
* plugins.
|
|
*/
|
|
void setAnalysisUpdateContentParams(AnalysisUpdateContentParams params) {
|
|
for (PluginInfo plugin in _pluginMap.values) {
|
|
plugin.sendRequest(params);
|
|
}
|
|
Map<String, dynamic> files = params.files;
|
|
for (String file in files.keys) {
|
|
Object overlay = files[file];
|
|
if (overlay is RemoveContentOverlay) {
|
|
_overlayState.remove(file);
|
|
} else if (overlay is AddContentOverlay) {
|
|
_overlayState[file] = overlay;
|
|
} else if (overlay is ChangeContentOverlay) {
|
|
AddContentOverlay previousOverlay = _overlayState[file];
|
|
String newContent =
|
|
SourceEdit.applySequence(previousOverlay.content, overlay.edits);
|
|
_overlayState[file] = new AddContentOverlay(newContent);
|
|
} else {
|
|
throw new ArgumentError(
|
|
'Invalid class of overlay: ${overlay.runtimeType}');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop all of the plugins that are currently running.
|
|
*/
|
|
Future<List<Null>> stopAll() {
|
|
return Future.wait(_pluginMap.values.map((PluginInfo info) => info.stop()));
|
|
}
|
|
|
|
/**
|
|
* Whitelist all plugins.
|
|
*/
|
|
@visibleForTesting
|
|
void whitelistEverything() {
|
|
_whitelistGlobs = <Glob>[
|
|
new Glob(resourceProvider.pathContext.separator, '**/*')
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 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, Workspace workspace}) {
|
|
File pluginFile = pluginFolder
|
|
.getChildAssumingFolder('bin')
|
|
.getChildAssumingFile('plugin.dart');
|
|
if (!pluginFile.exists) {
|
|
return null;
|
|
}
|
|
File packagesFile = pluginFolder.getChildAssumingFile('.packages');
|
|
if (!packagesFile.exists) {
|
|
if (runPub) {
|
|
String vmPath = Platform.executable;
|
|
String pubPath = path.join(path.dirname(vmPath), 'pub');
|
|
ProcessResult result = Process.runSync(pubPath, <String>['get'],
|
|
stderrEncoding: UTF8,
|
|
stdoutEncoding: UTF8,
|
|
workingDirectory: pluginFolder.path);
|
|
if (result.exitCode != 0) {
|
|
StringBuffer buffer = new StringBuffer();
|
|
buffer.writeln('Failed to run pub get');
|
|
buffer.writeln(' pluginFolder = ${pluginFolder.path}');
|
|
buffer.writeln(' exitCode = ${result.exitCode}');
|
|
buffer.writeln(' stdout = ${result.stdout}');
|
|
buffer.writeln(' stderr = ${result.stderr}');
|
|
instrumentationService.logError(buffer.toString());
|
|
}
|
|
if (!packagesFile.exists) {
|
|
packagesFile = null;
|
|
}
|
|
} else if (workspace != null) {
|
|
packagesFile =
|
|
_createPackagesFile(pluginFolder, workspace.packageUriResolver);
|
|
} else {
|
|
packagesFile = null;
|
|
}
|
|
}
|
|
if (packagesFile == null) {
|
|
return null;
|
|
}
|
|
return <String>[pluginFile.path, packagesFile.path];
|
|
}
|
|
|
|
WatchEventType _convertChangeType(watcher.ChangeType type) {
|
|
switch (type) {
|
|
case watcher.ChangeType.ADD:
|
|
return WatchEventType.ADD;
|
|
case watcher.ChangeType.MODIFY:
|
|
return WatchEventType.MODIFY;
|
|
case watcher.ChangeType.REMOVE:
|
|
return WatchEventType.REMOVE;
|
|
default:
|
|
throw new StateError('Unknown change type: $type');
|
|
}
|
|
}
|
|
|
|
WatchEvent _convertWatchEvent(watcher.WatchEvent watchEvent) {
|
|
return new WatchEvent(_convertChangeType(watchEvent.type), watchEvent.path);
|
|
}
|
|
|
|
/**
|
|
* Return a temporary `.packages` file that is appropriate for the plugin in
|
|
* the given [pluginFolder]. The [packageUriResolver] is used to determine the
|
|
* location of the packages that need to be included in the packages file.
|
|
*/
|
|
File _createPackagesFile(
|
|
Folder pluginFolder, UriResolver packageUriResolver) {
|
|
String pluginPath = pluginFolder.path;
|
|
Folder stateFolder = resourceProvider.getStateLocation('.plugin_manager');
|
|
String stateName = _uniqueDirectoryName(pluginPath) + '.packages';
|
|
File packagesFile = stateFolder.getChildAssumingFile(stateName);
|
|
if (!packagesFile.exists) {
|
|
File pluginPubspec = pluginFolder.getChildAssumingFile('pubspec.yaml');
|
|
if (!pluginPubspec.exists) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
Map<String, String> visitedPackages = <String, String>{};
|
|
path.Context context = resourceProvider.pathContext;
|
|
visitedPackages[context.basename(pluginPath)] =
|
|
context.join(pluginFolder.path, 'lib');
|
|
List<File> pubspecFiles = <File>[];
|
|
pubspecFiles.add(pluginPubspec);
|
|
while (pubspecFiles.isNotEmpty) {
|
|
File pubspecFile = pubspecFiles.removeLast();
|
|
for (String packageName in _readDependecies(pubspecFile)) {
|
|
if (!visitedPackages.containsKey(packageName)) {
|
|
Uri uri = Uri.parse('package:$packageName/$packageName.dart');
|
|
Source packageSource = packageUriResolver.resolveAbsolute(uri);
|
|
String libDirPath = context.dirname(packageSource.fullName);
|
|
visitedPackages[packageName] = libDirPath;
|
|
String pubspecPath =
|
|
context.join(context.dirname(libDirPath), 'pubspec.yaml');
|
|
pubspecFiles.add(resourceProvider.getFile(pubspecPath));
|
|
}
|
|
}
|
|
}
|
|
|
|
StringBuffer buffer = new StringBuffer();
|
|
visitedPackages.forEach((String name, String path) {
|
|
buffer.write(name);
|
|
buffer.write(':');
|
|
buffer.writeln(new Uri.file(path));
|
|
});
|
|
packagesFile.writeAsStringSync(buffer.toString());
|
|
} catch (exception) {
|
|
// If we are not able to produce a .packages file, return null so that
|
|
// callers will not try to load the plugin.
|
|
return null;
|
|
}
|
|
}
|
|
return packagesFile;
|
|
}
|
|
|
|
/**
|
|
* Return `true` if the plugin with the given [path] has been whitelisted.
|
|
*/
|
|
bool _isWhitelisted(String path) {
|
|
for (Glob glob in _whitelistGlobs) {
|
|
if (glob.matches(path)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Return the names of packages that are listed as dependencies in the given
|
|
* [pubspecFile].
|
|
*/
|
|
Iterable<String> _readDependecies(File pubspecFile) {
|
|
YamlDocument document = loadYamlDocument(pubspecFile.readAsStringSync(),
|
|
sourceUrl: pubspecFile.toUri());
|
|
YamlNode contents = document.contents;
|
|
if (contents is YamlMap) {
|
|
YamlNode dependencies = contents['dependencies'];
|
|
if (dependencies is YamlMap) {
|
|
return dependencies.keys;
|
|
}
|
|
}
|
|
return const <String>[];
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* Record the fact that the given [plugin] responded to a request with the
|
|
* given [method] in the given [time].
|
|
*/
|
|
static void recordResponseTime(PluginInfo plugin, String method, int time) {
|
|
pluginResponseTimes
|
|
.putIfAbsent(plugin, () => <String, List<int>>{})
|
|
.putIfAbsent(method, () => <int>[])
|
|
.add(time);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Information about the execution a single plugin.
|
|
*/
|
|
@visibleForTesting
|
|
class PluginSession {
|
|
/**
|
|
* The maximum number of milliseconds that server should wait for a response
|
|
* from a plugin before deciding that the plugin is hung.
|
|
*/
|
|
static const Duration MAXIMUM_RESPONSE_TIME = const Duration(minutes: 2);
|
|
|
|
/**
|
|
* The length of time to wait after sending a 'plugin.shutdown' request before
|
|
* a failure to terminate will cause the isolate to be killed.
|
|
*/
|
|
static const Duration WAIT_FOR_SHUTDOWN_DURATION =
|
|
const Duration(seconds: 10);
|
|
|
|
/**
|
|
* 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, _PendingRequest> pendingRequests = <String, _PendingRequest>{};
|
|
|
|
/**
|
|
* 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) {
|
|
if (notification.event == PLUGIN_NOTIFICATION_ERROR) {
|
|
PluginErrorParams params =
|
|
new PluginErrorParams.fromNotification(notification);
|
|
if (params.isFatal) {
|
|
info.stop();
|
|
stop();
|
|
}
|
|
}
|
|
info.notificationManager
|
|
.handlePluginNotification(info.pluginId, 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.
|
|
info.instrumentationService.logPluginException(
|
|
info.data, errorPair[0], new StackTrace.fromString(errorPair[1]));
|
|
}
|
|
|
|
/**
|
|
* Handle a [response] from the plugin by completing the future that was
|
|
* created when the request was sent.
|
|
*/
|
|
void handleResponse(Response response) {
|
|
_PendingRequest requestData = pendingRequests.remove(response.id);
|
|
int responseTime = new DateTime.now().millisecondsSinceEpoch;
|
|
int duration = responseTime - requestData.requestTime;
|
|
PluginManager.recordResponseTime(info, requestData.method, duration);
|
|
Completer<Response> completer = requestData.completer;
|
|
if (completer != null) {
|
|
completer.complete(response);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return `true` if there are any requests that have not been responded to
|
|
* within the maximum allowed amount of time.
|
|
*/
|
|
bool isNonResponsive() {
|
|
// TODO(brianwilkerson) Figure out when to invoke this method in order to
|
|
// identify non-responsive plugins and kill them.
|
|
int cutOffTime = new DateTime.now().millisecondsSinceEpoch -
|
|
MAXIMUM_RESPONSE_TIME.inMilliseconds;
|
|
for (var requestData in pendingRequests.values) {
|
|
if (requestData.requestTime < cutOffTime) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
int requestTime = new DateTime.now().millisecondsSinceEpoch;
|
|
Request request = parameters.toRequest(id);
|
|
pendingRequests[id] =
|
|
new _PendingRequest(request.method, requestTime, completer);
|
|
channel.sendRequest(request);
|
|
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, String sdkPath) 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 = info._createChannel();
|
|
await channel.listen(handleResponse, handleNotification,
|
|
onDone: handleOnDone, onError: handleOnError);
|
|
if (channel == null) {
|
|
// If there is an error when starting the isolate, the channel will invoke
|
|
// handleOnDone, which will cause `channel` to be set to `null`.
|
|
return false;
|
|
}
|
|
Response response = await sendRequest(new PluginVersionCheckParams(
|
|
byteStorePath ?? '', sdkPath, '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.');
|
|
}
|
|
sendRequest(new PluginShutdownParams());
|
|
new Future.delayed(WAIT_FOR_SHUTDOWN_DURATION, () {
|
|
if (channel != null) {
|
|
channel.kill();
|
|
channel = null;
|
|
}
|
|
});
|
|
return pluginStoppedCompleter.future;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Information about a request that has been sent but for which a response has
|
|
* not yet been received.
|
|
*/
|
|
class _PendingRequest {
|
|
/**
|
|
* The method of the request.
|
|
*/
|
|
final String method;
|
|
|
|
/**
|
|
* The time at which the request was sent to the plugin.
|
|
*/
|
|
final int requestTime;
|
|
|
|
/**
|
|
* The completer that will be used to complete the future when the response is
|
|
* received from the plugin.
|
|
*/
|
|
final Completer<Response> completer;
|
|
|
|
/**
|
|
* Initialize a pending request.
|
|
*/
|
|
_PendingRequest(this.method, this.requestTime, this.completer);
|
|
}
|