| // 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/exception/exception.dart'; | |
| import 'package:analyzer/file_system/file_system.dart'; | |
| import 'package:analyzer/instrumentation/instrumentation.dart'; | |
| import 'package:analyzer/src/context/context_root.dart' as analyzer; | |
| 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 | |
| bool get canBeStarted => executionPath != null; | |
| @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); | |
| } | |
| } | |
| /** | |
| * An indication of a problem with the execution of a plugin that occurs prior | |
| * to the execution of the plugin's entry point in an isolate. | |
| */ | |
| class PluginException implements Exception { | |
| /** | |
| * A message describing the problem. | |
| */ | |
| final String message; | |
| /** | |
| * Initialize a newly created exception to have the given [message]. | |
| */ | |
| PluginException(this.message); | |
| @override | |
| String toString() => message; | |
| } | |
| /** | |
| * 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; | |
| /** | |
| * The exception that occurred that prevented the plugin from being started, | |
| * or `null` if there was no exception (possibly because no attempt has yet | |
| * been made to start the plugin). | |
| */ | |
| CaughtException exception; | |
| /** | |
| * Initialize the newly created information about a plugin. | |
| */ | |
| PluginInfo(this.notificationManager, this.instrumentationService); | |
| /** | |
| * Return `true` if this plugin can be started, or `false` if there is a | |
| * reason why it cannot be started. For example, a plugin cannot be started if | |
| * there was an error with a previous attempt to start running it or if the | |
| * plugin is not correctly configured. | |
| */ | |
| bool get canBeStarted => true; | |
| /** | |
| * 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 { | |
| // TODO(brianwilkerson) Determine whether this await is necessary. | |
| await null; | |
| 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 and return 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 console environment key used by the pub tool. | |
| */ | |
| static const String _pubEnvironmentKey = 'PUB_ENVIRONMENT'; | |
| /** | |
| * 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; | |
| /** | |
| * 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); | |
| /** | |
| * Return a list of all of the plugins that are currently known. | |
| */ | |
| 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 { | |
| // TODO(brianwilkerson) Determine whether this await is necessary. | |
| await null; | |
| PluginInfo plugin = _pluginMap[path]; | |
| bool isNew = plugin == null; | |
| if (isNew) { | |
| List<String> pluginPaths; | |
| try { | |
| pluginPaths = pathsFor(path); | |
| } catch (exception, stackTrace) { | |
| plugin = new DiscoveredPluginInfo( | |
| path, null, null, notificationManager, instrumentationService); | |
| plugin.exception = new CaughtException(exception, stackTrace); | |
| _pluginMap[path] = plugin; | |
| return; | |
| } | |
| plugin = new DiscoveredPluginInfo(path, pluginPaths[0], pluginPaths[1], | |
| notificationManager, instrumentationService); | |
| _pluginMap[path] = plugin; | |
| if (pluginPaths[0] != null) { | |
| try { | |
| PluginSession session = await plugin.start(byteStorePath, sdkPath); | |
| session?.onDone?.then((_) { | |
| _pluginMap.remove(path); | |
| }); | |
| } catch (exception, stackTrace) { | |
| // Record the exception (for debugging purposes) and record the fact | |
| // that we should not try to communicate with the plugin. | |
| plugin.exception = new CaughtException(exception, stackTrace); | |
| isNew = false; | |
| } | |
| } | |
| } | |
| 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 { | |
| // TODO(brianwilkerson) Determine whether this await is necessary. | |
| await null; | |
| 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 != null && | |
| session.interestingFiles.any(matches)) { | |
| // The list of interesting file globs is `null` if the plugin has not | |
| // yet responded to the plugin.versionCheck request. If that happens | |
| // then the plugin hasn't had a chance to analyze anything yet, and | |
| // hence it does not needed to get watch events. | |
| 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]. Throw a [PluginException] if there is a problem that | |
| * prevents the plugin from being executing. | |
| */ | |
| @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; | |
| } | |
| /** | |
| * Record a failure to run the plugin associated with the host package with | |
| * the given [hostPackageName]. The failure is described by the [message], and | |
| * is expected to have occurred before a path could be computed, and hence | |
| * before [addPluginToContextRoot] could be invoked. | |
| */ | |
| void recordPluginFailure(String hostPackageName, String message) { | |
| try { | |
| throw new PluginException(message); | |
| } catch (exception, stackTrace) { | |
| String pluginPath = | |
| path.join(hostPackageName, 'tools', 'analyzer_plugin'); | |
| DiscoveredPluginInfo plugin = new DiscoveredPluginInfo( | |
| pluginPath, null, null, notificationManager, instrumentationService); | |
| plugin.exception = new CaughtException(exception, stackTrace); | |
| _pluginMap[pluginPath] = plugin; | |
| } | |
| } | |
| /** | |
| * 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 { | |
| // TODO(brianwilkerson) Determine whether this await is necessary. | |
| await null; | |
| 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())); | |
| } | |
| /** | |
| * 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) { | |
| throw new PluginException('File "${pluginFile.path}" does not exist.'); | |
| } | |
| String reason; | |
| File packagesFile = pluginFolder.getChildAssumingFile('.packages'); | |
| bool packagesFilePreExists = packagesFile.exists; | |
| if (runPub) { | |
| String vmPath = Platform.executable; | |
| String pubPath = path.join(path.dirname(vmPath), 'pub'); | |
| if (Platform.isWindows) { | |
| // Process.run requires the `.bat` suffix on Windows | |
| pubPath = '$pubPath.bat'; | |
| } | |
| String pubSubcommand = packagesFilePreExists ? 'upgrade' : 'get'; | |
| ProcessResult result = Process.runSync(pubPath, <String>[pubSubcommand], | |
| stderrEncoding: utf8, | |
| stdoutEncoding: utf8, | |
| workingDirectory: pluginFolder.path, | |
| environment: {_pubEnvironmentKey: _getPubEnvironmentValue()}); | |
| 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}'); | |
| reason = buffer.toString(); | |
| instrumentationService.logError(reason); | |
| } | |
| if (!packagesFile.exists) { | |
| reason ??= 'File "${packagesFile.path}" does not exist.'; | |
| packagesFile = null; | |
| } | |
| } else if (!packagesFilePreExists) { | |
| if (workspace != null) { | |
| packagesFile = | |
| _createPackagesFile(pluginFolder, workspace.packageUriResolver); | |
| if (packagesFile == null) { | |
| reason = 'Could not create .packages file in workspace $workspace.'; | |
| } | |
| } else { | |
| reason = 'Could not create "${packagesFile.path}".'; | |
| packagesFile = null; | |
| } | |
| } | |
| if (packagesFile == null) { | |
| throw new PluginException(reason); | |
| } | |
| 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 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.cast<String>(); | |
| } | |
| } | |
| 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); | |
| } | |
| /** | |
| * Returns the environment value that should be used when running pub. | |
| * | |
| * Includes any existing environment value, if one exists. | |
| */ | |
| static String _getPubEnvironmentValue() { | |
| // DO NOT update this function without contacting kevmoo. | |
| // We have server-side tooling that assumes the values are consistent. | |
| var values = <String>[]; | |
| var existing = Platform.environment[_pubEnvironmentKey]; | |
| // If there is an existing value for this var, make sure to include it. | |
| if ((existing != null) && existing.isNotEmpty) { | |
| values.add(existing); | |
| } | |
| values.add('analysis_server.plugin_manager'); | |
| return values.join(':'); | |
| } | |
| } | |
| /** | |
| * 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() { | |
| if (channel != null) { | |
| channel.close(); | |
| channel = null; | |
| } | |
| pluginStoppedCompleter.complete(null); | |
| } | |
| /** | |
| * Handle the fact that an unhandled error has occurred in the plugin. | |
| */ | |
| void handleOnError(dynamic error) { | |
| List<String> errorPair = (error as List).cast<String>(); | |
| StackTrace stackTrace = new StackTrace.fromString(errorPair[1]); | |
| info.exception = | |
| new CaughtException(new PluginException(errorPair[0]), stackTrace); | |
| info.instrumentationService | |
| .logPluginException(info.data, errorPair[0], stackTrace); | |
| } | |
| /** | |
| * 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 { | |
| // TODO(brianwilkerson) Determine whether this await is necessary. | |
| await null; | |
| 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) { | |
| info.exception = new CaughtException( | |
| new PluginException('Plugin is not compatible.'), null); | |
| return false; | |
| } | |
| if (!info.canBeStarted) { | |
| info.exception = new CaughtException( | |
| new PluginException('Plugin cannot be started.'), null); | |
| 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`. | |
| info.exception ??= new CaughtException( | |
| new PluginException('Unrecorded error while starting the plugin.'), | |
| 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()); | |
| info.exception = new CaughtException( | |
| new PluginException('Plugin is not compatible.'), null); | |
| 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); | |
| } |