| // 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); |
| } |