676 lines
22 KiB
Dart
676 lines
22 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:io' show Platform;
|
|
|
|
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/util/glob.dart';
|
|
import 'package:analyzer_plugin/channel/channel.dart';
|
|
import 'package:analyzer_plugin/protocol/protocol.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;
|
|
|
|
/**
|
|
* Information about a single plugin.
|
|
*/
|
|
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<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.path, this.executionPath, this.packagesPath,
|
|
this.notificationManager, this.instrumentationService);
|
|
|
|
/**
|
|
* Return the data known about this plugin.
|
|
*/
|
|
PluginData get data =>
|
|
new PluginData(path, currentSession?.name, currentSession?.version);
|
|
|
|
/**
|
|
* 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();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
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
|
|
.map((analyzer.ContextRoot contextRoot) =>
|
|
new ContextRoot(contextRoot.root, contextRoot.exclude))
|
|
.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>{};
|
|
|
|
/**
|
|
* 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.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(
|
|
analyzer.ContextRoot contextRoot, String path) async {
|
|
PluginInfo plugin = _pluginMap[path];
|
|
bool isNew = plugin == null;
|
|
if (isNew) {
|
|
List<String> pluginPaths = _pathsFor(path);
|
|
if (pluginPaths == null) {
|
|
return;
|
|
}
|
|
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);
|
|
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(path.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 &&
|
|
path.isWithin(plugin.path, filePath) &&
|
|
session.interestingFiles.any(matches)) {
|
|
event ??= _convertWatchEvent(watchEvent);
|
|
AnalysisHandleWatchEventsParams params =
|
|
new AnalysisHandleWatchEventsParams([event]);
|
|
responses.add(session.sendRequest(params));
|
|
}
|
|
}
|
|
return responses;
|
|
}
|
|
|
|
/**
|
|
* 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.contextRoots.isEmpty) {
|
|
_pluginMap.remove(plugin.path);
|
|
plugin.stop();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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()));
|
|
}
|
|
|
|
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 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;
|
|
}
|
|
} else {
|
|
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) {
|
|
if (notification.event == PLUGIN_NOTIFICATION_ERROR) {
|
|
PluginErrorParams params =
|
|
new PluginErrorParams.fromNotification(notification);
|
|
if (params.isFatal) {
|
|
info.stop();
|
|
stop();
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
}
|