| // 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. | |
| /// @docImport 'package:analysis_server/src/plugin/plugin_watcher.dart'; | |
| /// @docImport 'package:analysis_server_plugin/src/plugin_server.dart'; | |
| library; | |
| import 'dart:async'; | |
| import 'dart:convert'; | |
| import 'dart:io' show ProcessResult; | |
| import 'package:analysis_server/src/analytics/percentile_calculator.dart'; | |
| import 'package:analysis_server/src/plugin/notification_manager.dart'; | |
| import 'package:analysis_server/src/plugin/plugin_isolate.dart'; | |
| import 'package:analysis_server/src/session_logger/session_logger.dart'; | |
| import 'package:analysis_server/src/utilities/process.dart'; | |
| import 'package:analysis_server/src/utilities/sdk.dart'; | |
| import 'package:analyzer/dart/analysis/context_root.dart' as analyzer; | |
| import 'package:analyzer/exception/exception.dart'; | |
| import 'package:analyzer/file_system/file_system.dart'; | |
| import 'package:analyzer/instrumentation/instrumentation.dart'; | |
| import 'package:analyzer/src/generated/source.dart'; | |
| import 'package:analyzer/src/util/file_paths.dart' as file_paths; | |
| import 'package:analyzer/src/util/glob.dart'; | |
| import 'package:analyzer/src/util/platform_info.dart'; | |
| import 'package:analyzer/src/workspace/blaze.dart'; | |
| import 'package:analyzer/src/workspace/workspace.dart'; | |
| import 'package:analyzer_plugin/protocol/protocol.dart'; | |
| import 'package:analyzer_plugin/protocol/protocol_common.dart'; | |
| import 'package:analyzer_plugin/protocol/protocol_generated.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:watcher/watcher.dart' as watcher; | |
| import 'package:yaml/yaml.dart'; | |
| const _builtAsAot = bool.fromEnvironment('built_as_aot'); | |
| /// 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]. | |
| new(this.message); | |
| @override | |
| String toString() => message; | |
| } | |
| /// The necessary files that define an analyzer plugin on disk. | |
| class PluginFiles { | |
| /// The plugin entry point. | |
| final File execution; | |
| /// The plugin package config file. | |
| final File packageConfig; | |
| new(this.execution, this.packageConfig); | |
| } | |
| /// 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<PluginIsolate, Map<String, PercentileCalculator>> | |
| pluginResponseTimes = <PluginIsolate, Map<String, PercentileCalculator>>{}; | |
| /// 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 AbstractNotificationManager _notificationManager; | |
| /// The instrumentation service that is being used by the analysis server. | |
| final InstrumentationService instrumentationService; | |
| /// The session logger that is being used by the analysis server. | |
| final SessionLogger sessionLogger; | |
| /// A table mapping the paths of plugins to information about those plugins. | |
| final Map<String, PluginIsolate> _pluginMap = <String, PluginIsolate>{}; | |
| /// The parameters for the last 'analysis.setAnalysisRoots' 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. | |
| AnalysisSetAnalysisRootsParams? _analysisSetAnalysisRootsParams; | |
| /// 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. | |
| final Map<String, AddContentOverlay> _overlayState = {}; | |
| final StreamController<void> _pluginsChanged = StreamController.broadcast(); | |
| /// Whether plugins are "initialized." | |
| /// | |
| /// Plugins are declared to be initialized either (a) when the [PluginWatcher] | |
| /// has determined no plugins are configured to be run, or (b) when the | |
| /// plugins are configured and the first status notification is received by | |
| /// the analysis server. | |
| /// | |
| /// It is important that this completer always eventually completes and is not | |
| /// left dangling (for example if plugins are invalid, fail to load, etc) | |
| /// because tools like 'dart analyze' may wait on it via the | |
| /// 'dart/workspace/analysis/complete' request. | |
| final Completer<void> initializedCompleter = Completer(); | |
| final ProcessRunner _processRunner; | |
| /// The set of context root paths with no configured _new_ plugins. | |
| final contextRootsWithNoPlugins = <String>{}; | |
| /// Initializes a newly created plugin manager. | |
| /// | |
| /// The notifications from the running plugins will be handled by the given | |
| /// [_notificationManager]. | |
| new( | |
| this._resourceProvider, | |
| this._byteStorePath, | |
| this._sdkPath, | |
| this._notificationManager, | |
| this.instrumentationService, | |
| this.sessionLogger, { | |
| this._processRunner = const ProcessRunner(), | |
| }); | |
| /// All of the legacy plugins that are currently known. | |
| List<PluginIsolate> get legacyPluginIsolates => | |
| pluginIsolates.where((p) => p.isLegacy).toList(); | |
| /// All of the "new" plugins that are currently known. | |
| List<PluginIsolate> get newPluginIsolates => | |
| pluginIsolates.where((p) => !p.isLegacy).toList(); | |
| /// All of the plugins that are currently known. | |
| List<PluginIsolate> get pluginIsolates => _pluginMap.values.toList(); | |
| /// Stream emitting an event when known [pluginIsolates] change. | |
| Stream<void> get pluginsChanged => _pluginsChanged.stream; | |
| /// Adds 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. | |
| /// | |
| /// Specify whether this is a legacy plugin with [isLegacyPlugin]. | |
| Future<void> addPluginToContextRoot( | |
| analyzer.ContextRoot contextRoot, | |
| String path, { | |
| required bool isLegacyPlugin, | |
| }) async { | |
| var pluginIsolate = _pluginMap[path]; | |
| if (pluginIsolate != null) { | |
| pluginIsolate.addContextRoot(contextRoot); | |
| return; | |
| } | |
| var startedSuccessfully = true; | |
| PluginFiles pluginFiles; | |
| try { | |
| pluginFiles = filesFor(path, isLegacyPlugin: isLegacyPlugin); | |
| } catch (exception, stackTrace) { | |
| pluginIsolate = PluginIsolate( | |
| path, | |
| null, | |
| null, | |
| _notificationManager, | |
| instrumentationService, | |
| sessionLogger, | |
| isLegacy: isLegacyPlugin, | |
| ); | |
| pluginIsolate.reportException(CaughtException(exception, stackTrace)); | |
| _pluginMap[path] = pluginIsolate; | |
| return; | |
| } | |
| pluginIsolate = PluginIsolate( | |
| path, | |
| pluginFiles.execution.path, | |
| pluginFiles.packageConfig.path, | |
| _notificationManager, | |
| instrumentationService, | |
| sessionLogger, | |
| isLegacy: isLegacyPlugin, | |
| ); | |
| try { | |
| instrumentationService.logInfo('Starting plugin "$pluginIsolate"'); | |
| sessionLogger.logInfo('Starting plugin "$pluginIsolate"'); | |
| var session = await pluginIsolate.start(_byteStorePath, _sdkPath); | |
| unawaited( | |
| session?.onDone.then((_) { | |
| if (_pluginMap[path] == pluginIsolate) { | |
| _pluginMap.remove(path); | |
| _notifyPluginsChanged(); | |
| } | |
| }), | |
| ); | |
| } catch (exception, stackTrace) { | |
| // Record the exception (for debugging purposes) and record the fact | |
| // that we should not try to communicate with the plugin. | |
| pluginIsolate.reportException(CaughtException(exception, stackTrace)); | |
| startedSuccessfully = false; | |
| } | |
| _pluginMap[path] = pluginIsolate; | |
| _notifyPluginsChanged(); | |
| if (!isLegacyPlugin && _analysisSetAnalysisRootsParams != null) { | |
| pluginIsolate.setAnalysisRoots(_analysisSetAnalysisRootsParams!); | |
| } | |
| pluginIsolate.addContextRoot(contextRoot); | |
| if (startedSuccessfully) { | |
| // Send these cached requests to the restarted plugin in order to catch it | |
| // back up to the working state. | |
| var cachedRequests = [ | |
| ?_analysisSetSubscriptionsParams, | |
| if (_overlayState.isNotEmpty) | |
| AnalysisUpdateContentParams(_overlayState), | |
| ?_analysisSetPriorityFilesParams, | |
| ]; | |
| for (var cachedRequest in cachedRequests) { | |
| pluginIsolate.sendRequest(cachedRequest); | |
| } | |
| } | |
| } | |
| /// 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<PluginIsolate, Future<Response>> broadcastRequest( | |
| RequestParams params, { | |
| analyzer.ContextRoot? contextRoot, | |
| }) { | |
| var pluginIsolates = pluginsForContextRoot(contextRoot); | |
| var responseMap = <PluginIsolate, Future<Response>>{}; | |
| for (var pluginIsolate in pluginIsolates) { | |
| var request = pluginIsolate.currentSession?.sendRequest(params); | |
| // Only add an entry to the map if we have sent a request. | |
| if (request != null) { | |
| responseMap[pluginIsolate] = request; | |
| } | |
| } | |
| return responseMap; | |
| } | |
| /// Broadcasts the given [watchEvent] to all of the plugins that are analyzing | |
| /// in contexts containing the file associated with the event. | |
| /// | |
| /// Returns a list containing futures that will complete when each of the | |
| /// plugins have sent a response. | |
| List<Future<Response>> broadcastWatchEvent(watcher.WatchEvent watchEvent) { | |
| var filePath = watchEvent.path; | |
| WatchEvent? event; | |
| var responses = <Future<Response>>[]; | |
| var separator = _resourceProvider.pathContext.separator; | |
| for (var pluginIsolate in _pluginMap.values) { | |
| var session = pluginIsolate.currentSession; | |
| if (session == null) continue; | |
| if (!pluginIsolate.isAnalyzing(filePath)) continue; | |
| var interestingGlobs = session.interestingFileGlobs; | |
| // The list of interesting file globs is `null` if the isolate has not yet | |
| // responded to the 'plugin.versionCheck' request. If that happens, then | |
| // the isolate hasn't had a chance to analyze anything yet; hence, it | |
| // it does not need to get watch events, yet. | |
| if (interestingGlobs == null) continue; | |
| // Canonicalize the globs. If a glob does not start with '**/', then | |
| // prepend '**/' to the glob. | |
| interestingGlobs = [ | |
| for (var g in interestingGlobs) | |
| if (g.startsWith('**/')) g else '**/$g', | |
| ]; | |
| if (!interestingGlobs.any((g) => Glob(separator, g).matches(filePath))) { | |
| continue; | |
| } | |
| event ??= _convertWatchEvent(watchEvent); | |
| var params = AnalysisHandleWatchEventsParams([event]); | |
| responses.add(session.sendRequest(params)); | |
| } | |
| return responses; | |
| } | |
| /// Returns the files associated with the plugin at the given [pluginPath]. | |
| /// | |
| /// In some cases, the plugin's sources are copied to a special directory. If | |
| /// [pluginPath] does not include a `pubspec.yaml` file, we do not. If | |
| /// [pluginPath] exists in a [BlazeWorkspace], we do not. | |
| /// | |
| /// Throws a [PluginException] if there is a problem that prevents the plugin | |
| /// from being executing. | |
| /// | |
| /// [builtAsAot] can be passed in during a test, to simulate a different flow. | |
| @visibleForTesting | |
| PluginFiles filesFor( | |
| String pluginPath, { | |
| required bool isLegacyPlugin, | |
| @visibleForTesting bool builtAsAot = _builtAsAot, | |
| }) { | |
| var pluginFolder = _resourceProvider.getFolder(pluginPath); | |
| var pubspecFile = pluginFolder.getFile(file_paths.pubspecYaml); | |
| 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 _computeFiles(pluginFolder, builtAsAot: builtAsAot); | |
| } | |
| var workspace = BlazeWorkspace.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 _computeFiles( | |
| pluginFolder, | |
| builtAsAot: builtAsAot, | |
| workspace: workspace, | |
| ); | |
| } | |
| if (!isLegacyPlugin) { | |
| return _computeFiles( | |
| pluginFolder, | |
| builtAsAot: builtAsAot, | |
| pubCommand: 'upgrade', | |
| ); | |
| } | |
| // 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 we | |
| // copy the plugin contents, and the number of times we run `pub`. | |
| var parentFolder = pluginStateFolder(pluginPath); | |
| if (parentFolder.exists) { | |
| var executionFolder = parentFolder.getFolder(pluginFolder.shortName); | |
| return _computeFiles( | |
| executionFolder, | |
| builtAsAot: builtAsAot, | |
| pubCommand: 'upgrade', | |
| ); | |
| } | |
| var executionFolder = pluginFolder.copyTo(parentFolder); | |
| return _computeFiles( | |
| executionFolder, | |
| builtAsAot: builtAsAot, | |
| pubCommand: 'get', | |
| ); | |
| } | |
| /// Return a list of all of the plugin isolates that are currently associated | |
| /// with the given [contextRoot]. | |
| @visibleForTesting | |
| List<PluginIsolate> pluginsForContextRoot(analyzer.ContextRoot? contextRoot) { | |
| if (contextRoot == null) { | |
| return _pluginMap.values.toList(); | |
| } | |
| return [ | |
| for (var pluginIsolate in _pluginMap.values) | |
| if (pluginIsolate.contextRoots.contains(contextRoot)) pluginIsolate, | |
| ]; | |
| } | |
| /// Returns the "plugin state" folder for a plugin at [pluginPath]. | |
| /// | |
| /// This is a directory under the state location for '.plugin_manager', named | |
| /// with a hash based on [pluginPath]. | |
| Folder pluginStateFolder(String pluginPath) { | |
| var stateFolder = _resourceProvider.getStateLocation('.plugin_manager'); | |
| if (stateFolder == null) { | |
| throw PluginException('No state location, so plugin could not be copied'); | |
| } | |
| var stateName = _uniqueDirectoryName(pluginPath); | |
| return stateFolder.getFolder(stateName); | |
| } | |
| /// The path to the "plugin state" folder for a plugin at [pluginPath]. | |
| String pluginStateFolderPath(String pluginPath) => | |
| pluginStateFolder(pluginPath).path; | |
| /// The given [contextRoot] is no longer being analyzed. | |
| void removedContextRoot(analyzer.ContextRoot contextRoot) { | |
| var plugins = _pluginMap.values.toList(); | |
| for (var plugin in plugins) { | |
| plugin.removeContextRoot(contextRoot); | |
| if (plugin.contextRoots.isEmpty) { | |
| _pluginMap.remove(plugin.pluginId); | |
| _notifyPluginsChanged(); | |
| try { | |
| plugin.stop(); | |
| } catch (exception, stackTrace) { | |
| var silentException = SilentException( | |
| 'Issue stopping a plugin', | |
| exception, | |
| stackTrace, | |
| ); | |
| instrumentationService.logException(silentException); | |
| sessionLogger.logException(exception: silentException); | |
| } | |
| } | |
| } | |
| } | |
| /// Restart all currently running plugins. | |
| Future<void> restartPlugins() async { | |
| for (var plugin in _pluginMap.values.toList()) { | |
| if (plugin.currentSession != null) { | |
| // | |
| // Capture needed state. | |
| // | |
| var contextRoots = plugin.contextRoots; | |
| var path = plugin.pluginId; | |
| // | |
| // Stop the plugin. | |
| // | |
| await plugin.stop(); | |
| // | |
| // Restart the plugin. | |
| // | |
| _pluginMap[path] = plugin; | |
| var session = await plugin.start(_byteStorePath, _sdkPath); | |
| unawaited( | |
| session?.onDone.then((_) { | |
| _pluginMap.remove(path); | |
| }), | |
| ); | |
| // | |
| // Re-initialize the plugin. | |
| // | |
| plugin.addContextRoots(contextRoots); | |
| var analysisSetSubscriptionsParams = _analysisSetSubscriptionsParams; | |
| if (analysisSetSubscriptionsParams != null) { | |
| plugin.sendRequest(analysisSetSubscriptionsParams); | |
| } | |
| if (_overlayState.isNotEmpty) { | |
| plugin.sendRequest(AnalysisUpdateContentParams(_overlayState)); | |
| } | |
| var analysisSetPriorityFilesParams = _analysisSetPriorityFilesParams; | |
| if (analysisSetPriorityFilesParams != null) { | |
| plugin.sendRequest(analysisSetPriorityFilesParams); | |
| } | |
| } | |
| } | |
| } | |
| /// Sends a request based on the given [params] to existing _new_ (not legacy) | |
| /// plugins to set the analysis roots to those specified by [params]. | |
| /// | |
| /// As a side-effect, records the parameters so that they can be sent to any | |
| /// newly started plugins. | |
| void setAnalysisSetAnalysisRootsParams( | |
| AnalysisSetAnalysisRootsParams params, | |
| ) { | |
| for (var isolate in newPluginIsolates) { | |
| isolate.setAnalysisRoots(params); | |
| } | |
| _analysisSetAnalysisRootsParams = params; | |
| } | |
| /// Sends 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, records the parameters so that they can be sent to any | |
| /// newly started plugins. | |
| void setAnalysisSetPriorityFilesParams( | |
| AnalysisSetPriorityFilesParams params, | |
| ) { | |
| for (var plugin in _pluginMap.values) { | |
| plugin.sendRequest(params); | |
| } | |
| _analysisSetPriorityFilesParams = params; | |
| } | |
| /// Sends a request based on the given [params] to existing plugins to set the | |
| /// subscriptions to those specified by the [params]. | |
| /// | |
| /// As a side-effect, records the parameters so that they can be sent to any | |
| /// newly started plugins. | |
| void setAnalysisSetSubscriptionsParams( | |
| AnalysisSetSubscriptionsParams params, | |
| ) { | |
| for (var plugin in _pluginMap.values) { | |
| plugin.sendRequest(params); | |
| } | |
| _analysisSetSubscriptionsParams = params; | |
| } | |
| /// Sends 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, updates the overlay state so that it can be sent to any | |
| /// newly started plugins. | |
| void setAnalysisUpdateContentParams( | |
| AnalysisUpdateContentParams params, { | |
| String? precomputedNewContentForChange, | |
| }) { | |
| for (var plugin in _pluginMap.values) { | |
| plugin.sendRequest(params); | |
| } | |
| var files = params.files; | |
| for (var file in files.keys) { | |
| var overlay = files[file]; | |
| if (overlay is RemoveContentOverlay) { | |
| _overlayState.remove(file); | |
| } else if (overlay is AddContentOverlay) { | |
| _overlayState[file] = overlay; | |
| } else if (overlay is ChangeContentOverlay) { | |
| if (precomputedNewContentForChange != null) { | |
| _overlayState[file] = AddContentOverlay( | |
| precomputedNewContentForChange, | |
| ); | |
| } else { | |
| var previousOverlay = _overlayState[file]!; | |
| var newContent = SourceEdit.applySequence( | |
| previousOverlay.content, | |
| overlay.edits, | |
| ); | |
| _overlayState[file] = AddContentOverlay(newContent); | |
| } | |
| } else { | |
| throw ArgumentError('Invalid class of overlay: ${overlay.runtimeType}'); | |
| } | |
| } | |
| } | |
| /// Stops all of the plugin isolates that are currently running. | |
| Future<List<void>> stopAll() { | |
| return Future.wait( | |
| _pluginMap.values.map((pluginIsolate) async { | |
| try { | |
| await pluginIsolate.stop(); | |
| } catch (exception, stackTrace) { | |
| instrumentationService.logException(exception, stackTrace); | |
| sessionLogger.logException( | |
| exception: exception, | |
| stackTrace: stackTrace, | |
| ); | |
| } | |
| }), | |
| ); | |
| } | |
| /// Compiles [entrypoint] to an AOT snapshot and records timing to the | |
| /// instrumentation log. | |
| ProcessResult _compileAotSnapshot(File entrypoint) { | |
| instrumentationService.logInfo( | |
| 'Running "dart compile aot-snapshot $entrypoint".', | |
| ); | |
| sessionLogger.logInfo('Running "dart compile aot-snapshot $entrypoint".'); | |
| var stopwatch = Stopwatch()..start(); | |
| var depfile = entrypoint.parent.getFile('depfile.txt'); | |
| var aotSnapshotFile = entrypoint.parent.getFile('plugin.aot'); | |
| if (aotSnapshotFile.exists) { | |
| try { | |
| // Delete any existing AOT snapshot. On MacOS, sometimes this file | |
| // becomes quarantined due to a race condition with codesigning and | |
| // overwriting the file. | |
| aotSnapshotFile.delete(); | |
| } catch (e) { | |
| instrumentationService.logInfo( | |
| 'Could not delete existing AOT plugin entrypoint at ' | |
| '"$aotSnapshotFile": $e.', | |
| ); | |
| sessionLogger.logInfo( | |
| 'Could not delete existing AOT plugin entrypoint at ' | |
| '"$aotSnapshotFile": $e.', | |
| ); | |
| } | |
| } | |
| var result = _processRunner.runSync( | |
| sdk.dart, | |
| ['compile', 'aot-snapshot', '--depfile', depfile.path, entrypoint.path], | |
| stderrEncoding: utf8, | |
| stdoutEncoding: utf8, | |
| ); | |
| stopwatch.stop(); | |
| instrumentationService.logInfo( | |
| 'Running "dart compile aot-snapshot" took ${stopwatch.elapsed}.', | |
| ); | |
| sessionLogger.logInfo( | |
| 'Running "dart compile aot-snapshot" took ${stopwatch.elapsed}.', | |
| ); | |
| return result; | |
| } | |
| /// Compiles [pluginFile], in [pluginFolder], to an AOT snapshot, and returns | |
| /// the [File] for the snapshot. | |
| File _compileAsAot({required File pluginFile, required Folder pluginFolder}) { | |
| try { | |
| // Potentially use existing snapshot. | |
| var aotSnapshotFile = _existingAotSnapshot( | |
| resourceProvider: _resourceProvider, | |
| pluginFile: pluginFile, | |
| pluginFolder: pluginFolder, | |
| ); | |
| if (aotSnapshotFile != null) { | |
| instrumentationService.logInfo( | |
| 'Using existing plugin AOT snapshot at ' | |
| "'${aotSnapshotFile.path}'", | |
| ); | |
| sessionLogger.logInfo( | |
| 'Using existing plugin AOT snapshot at ' | |
| "'${aotSnapshotFile.path}'", | |
| ); | |
| return aotSnapshotFile; | |
| } | |
| } catch (error, stackTrace) { | |
| var exception = | |
| 'Exception while checking an existing plugin AOT snapshot: ' | |
| '"$error"\n$stackTrace'; | |
| instrumentationService.logException(exception); | |
| sessionLogger.logException(exception: exception); | |
| } | |
| // When the Dart Analysis Server is built as AOT, then all spawned | |
| // Isolates must also be built as AOT. | |
| var aotResult = _compileAotSnapshot(pluginFile); | |
| if (aotResult.exitCode != 0) { | |
| var buffer = StringBuffer(); | |
| buffer.writeln( | |
| 'Failed to compile "${pluginFile.path}" to an AOT snapshot.', | |
| ); | |
| var stderr = aotResult.stderr as String; | |
| if (stderr.contains('does not support build hooks')) { | |
| buffer.writeln( | |
| 'One of the plugins uses Dart build hooks (or depends on a package ' | |
| 'which uses them); this is currently not supported.', | |
| ); | |
| } | |
| buffer.writeln(' pluginFolder = ${pluginFolder.path}'); | |
| buffer.writeln(' exitCode = ${aotResult.exitCode}'); | |
| buffer.writeln(' stdout = ${aotResult.stdout}'); | |
| buffer.writeln(' stderr = $stderr'); | |
| var exceptionReason = buffer.toString(); | |
| instrumentationService.logError(exceptionReason); | |
| sessionLogger.logException(exception: exceptionReason); | |
| throw PluginException(exceptionReason); | |
| } | |
| return pluginFolder.getFolder('bin').getFile('plugin.aot'); | |
| } | |
| /// Computes the plugin files, given that the plugin should exist in | |
| /// [pluginFolder]. | |
| /// | |
| /// Runs `pub <pubCommand>` in [pluginFolder] if [pubCommand] is not `null`. | |
| PluginFiles _computeFiles( | |
| Folder pluginFolder, { | |
| required bool builtAsAot, | |
| String? pubCommand, | |
| Workspace? workspace, | |
| }) { | |
| var pluginFile = pluginFolder.getFolder('bin').getFile('plugin.dart'); | |
| if (!pluginFile.exists) { | |
| throw PluginException("File '${pluginFile.path}' does not exist."); | |
| } | |
| File? packageConfigFile = pluginFolder | |
| .getFolder(file_paths.dotDartTool) | |
| .getFile(file_paths.packageConfigJson); | |
| if (pubCommand != null) { | |
| var pubResult = _runPubCommand( | |
| pubCommand, | |
| workingDirectory: pluginFolder, | |
| ); | |
| String? exceptionReason; | |
| if (pubResult.exitCode != 0) { | |
| var buffer = StringBuffer(); | |
| buffer.writeln( | |
| 'An error occurred while setting up the analyzer plugin package at ' | |
| "'${pluginFolder.path}'. The `dart pub $pubCommand` command failed:", | |
| ); | |
| buffer.writeln(' exitCode = ${pubResult.exitCode}'); | |
| buffer.writeln(' stdout = ${pubResult.stdout}'); | |
| buffer.writeln(' stderr = ${pubResult.stderr}'); | |
| throw PluginException(buffer.toString()); | |
| } | |
| if (!packageConfigFile.exists) { | |
| exceptionReason ??= 'File "${packageConfigFile.path}" does not exist.'; | |
| throw PluginException(exceptionReason); | |
| } | |
| if (builtAsAot) { | |
| // Update the entrypoint path to be the AOT-compiled file. | |
| pluginFile = _compileAsAot( | |
| pluginFile: pluginFile, | |
| pluginFolder: pluginFolder, | |
| ); | |
| } | |
| return PluginFiles(pluginFile, packageConfigFile); | |
| } | |
| if (!packageConfigFile.exists) { | |
| if (workspace == null) { | |
| throw PluginException('Could not create "${packageConfigFile.path}".'); | |
| } | |
| packageConfigFile = _createPackageConfigFile( | |
| pluginFolder, | |
| workspace.packageUriResolver, | |
| ); | |
| if (packageConfigFile == null) { | |
| throw PluginException( | |
| "Could not create the '${file_paths.packageConfigJson}' file in " | |
| "the workspace at '$workspace'.", | |
| ); | |
| } | |
| } | |
| if (builtAsAot) { | |
| // Update the entrypoint path to be the AOT-compiled file. | |
| pluginFile = _compileAsAot( | |
| pluginFile: pluginFile, | |
| pluginFolder: pluginFolder, | |
| ); | |
| } | |
| return PluginFiles(pluginFile, packageConfigFile); | |
| } | |
| WatchEventType _convertChangeType(watcher.ChangeType type) { | |
| return switch (type) { | |
| watcher.ChangeType.ADD => WatchEventType.ADD, | |
| watcher.ChangeType.MODIFY => WatchEventType.MODIFY, | |
| watcher.ChangeType.REMOVE => WatchEventType.REMOVE, | |
| _ => throw StateError('Unknown change type: $type'), | |
| }; | |
| } | |
| WatchEvent _convertWatchEvent(watcher.WatchEvent watchEvent) { | |
| return WatchEvent(_convertChangeType(watchEvent.type), watchEvent.path); | |
| } | |
| /// Returns a temporary `package_config.json` 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 package config file. | |
| File? _createPackageConfigFile( | |
| Folder pluginFolder, | |
| UriResolver packageUriResolver, | |
| ) { | |
| var pluginPath = pluginFolder.path; | |
| var stateFolder = _resourceProvider.getStateLocation('.plugin_manager')!; | |
| var stateName = '${_uniqueDirectoryName(pluginPath)}.packages'; | |
| var packageConfigFile = stateFolder.getFile(stateName); | |
| if (!packageConfigFile.exists) { | |
| var pluginPubspec = pluginFolder.getFile(file_paths.pubspecYaml); | |
| if (!pluginPubspec.exists) { | |
| return null; | |
| } | |
| try { | |
| var visitedPackageNames = <String>{}; | |
| var packages = <_Package>[]; | |
| var context = _resourceProvider.pathContext; | |
| packages.add(_Package(context.basename(pluginPath), pluginFolder)); | |
| var pubspecFiles = <File>[]; | |
| pubspecFiles.add(pluginPubspec); | |
| while (pubspecFiles.isNotEmpty) { | |
| var pubspecFile = pubspecFiles.removeLast(); | |
| for (var packageName in _readDependencies(pubspecFile)) { | |
| if (visitedPackageNames.add(packageName)) { | |
| var uri = Uri.parse('package:$packageName/$packageName.dart'); | |
| var packageSource = packageUriResolver.resolveAbsolute(uri); | |
| if (packageSource != null) { | |
| var packageRoot = _resourceProvider | |
| .getFile(packageSource.fullName) | |
| .parent | |
| .parent; | |
| packages.add(_Package(packageName, packageRoot)); | |
| pubspecFiles.add(packageRoot.getFile(file_paths.pubspecYaml)); | |
| } | |
| } | |
| } | |
| } | |
| packages.sort((a, b) => a.name.compareTo(b.name)); | |
| var packageConfigContent = const JsonEncoder.withIndent(' ').convert({ | |
| 'configVersion': 2, | |
| 'packages': [ | |
| for (var package in packages) | |
| { | |
| 'name': package.name, | |
| 'rootUri': '${package.root.toUri()}', | |
| 'packageUri': 'lib/', | |
| }, | |
| ], | |
| }); | |
| packageConfigFile.writeAsStringSync('$packageConfigContent\n'); | |
| } catch (exception) { | |
| // If we are not able to produce a package config file, return `null` so | |
| // that callers will not try to load the plugin. | |
| return null; | |
| } | |
| } | |
| return packageConfigFile; | |
| } | |
| /// Returns a viable existing plugin AOT snapshot, if it exists and its | |
| /// modification timestamp is newer than all of its dependencies, and `null` | |
| /// otherwise. | |
| /// | |
| /// The dependencies of an AOT snapshot are the pubspec file, the | |
| /// entrypoint file, and all of the files referenced in the depfile which | |
| /// is generated by the `dart compile` command. | |
| File? _existingAotSnapshot({ | |
| required ResourceProvider resourceProvider, | |
| required File pluginFile, | |
| required Folder pluginFolder, | |
| }) { | |
| var aotSnapshotFile = pluginFolder.getFolder('bin').getFile('plugin.aot'); | |
| if (!aotSnapshotFile.exists) return null; | |
| var snapshotModificationStamp = aotSnapshotFile.modificationStamp; | |
| if (pluginFile.modificationStamp > snapshotModificationStamp) return null; | |
| var pubspecFile = pluginFolder.getFile(file_paths.pubspecYaml); | |
| if (pubspecFile.modificationStamp > snapshotModificationStamp) return null; | |
| var depfile = pluginFolder.getFolder('bin').getFile('depfile.txt'); | |
| if (!depfile.exists) return null; | |
| var content = depfile.readAsStringSync(); | |
| var dependencies = parseDepfile(content); | |
| if (dependencies == null) { | |
| // Malformed depfile content. | |
| return null; | |
| } | |
| for (var dependencyPath in dependencies) { | |
| var file = _resourceProvider.getFile(dependencyPath); | |
| if (!file.exists) { | |
| // Something has certainly changed on disk; do not use the cached | |
| // snapshot. | |
| return null; | |
| } | |
| if (file.modificationStamp > snapshotModificationStamp) { | |
| // Snapshot is stale. | |
| return null; | |
| } | |
| } | |
| return aotSnapshotFile; | |
| } | |
| void _notifyPluginsChanged() => _pluginsChanged.add(null); | |
| /// Return the names of packages that are listed as dependencies in the given | |
| /// [pubspecFile]. | |
| Iterable<String> _readDependencies(File pubspecFile) { | |
| var document = loadYamlDocument( | |
| pubspecFile.readAsStringSync(), | |
| sourceUrl: pubspecFile.toUri(), | |
| ); | |
| var contents = document.contents; | |
| if (contents is YamlMap) { | |
| var dependencies = contents['dependencies'] as YamlNode?; | |
| if (dependencies is YamlMap) { | |
| return dependencies.keys.cast<String>(); | |
| } | |
| } | |
| return const <String>[]; | |
| } | |
| /// Runs (and records timing to the instrumentation log) a Pub command | |
| /// [pubCommand] in [workingDirectory]. | |
| ProcessResult _runPubCommand( | |
| String pubCommand, { | |
| required Folder workingDirectory, | |
| }) { | |
| instrumentationService.logInfo( | |
| 'Running "pub $pubCommand" in "${workingDirectory.path}".', | |
| ); | |
| sessionLogger.logInfo( | |
| 'Running "pub $pubCommand" in "${workingDirectory.path}".', | |
| ); | |
| var stopwatch = Stopwatch()..start(); | |
| var result = _processRunner.runSync( | |
| sdk.dart, | |
| ['pub', pubCommand], | |
| workingDirectory: workingDirectory.path, | |
| environment: {_pubEnvironmentKey: _getPubEnvironmentValue()}, | |
| stderrEncoding: utf8, | |
| stdoutEncoding: utf8, | |
| ); | |
| stopwatch.stop(); | |
| instrumentationService.logInfo( | |
| 'Running "pub $pubCommand" took ${stopwatch.elapsed}.', | |
| ); | |
| sessionLogger.logInfo( | |
| 'Running "pub $pubCommand" took ${stopwatch.elapsed}.', | |
| ); | |
| return result; | |
| } | |
| /// Returns a hex-encoded MD5 signature of the given file [path]. | |
| String _uniqueDirectoryName(String path) { | |
| var bytes = md5.convert(path.codeUnits).bytes; | |
| return hex.encode(bytes); | |
| } | |
| /// Parses Ninja-style depfile content, returning a list of dependency paths. | |
| /// | |
| /// Returns `null` if the text is not valid depfile content. | |
| /// | |
| /// The format is: | |
| /// | |
| /// target: dependency1 dependency2 ... | |
| /// | |
| /// See https://ninja-build.org/manual.html#_depfile. | |
| @visibleForTesting | |
| static List<String>? parseDepfile(String content) { | |
| var colonIndex = content.indexOf(': '); | |
| if (colonIndex < 0) { | |
| // Not a valid depfile. | |
| return null; | |
| } | |
| var dependenciesString = content | |
| .substring(colonIndex + 1) | |
| .trimLeft() | |
| .replaceAll(RegExp(r'[\r\n]'), ''); | |
| var dependencies = <String>[]; | |
| var start = 0; | |
| while (start < dependenciesString.length) { | |
| var index = start; | |
| while (index < dependenciesString.length) { | |
| var char = dependenciesString[index]; | |
| if (char == ' ') { | |
| break; | |
| } else if (char == r'\') { | |
| index++; | |
| } | |
| index++; | |
| } | |
| dependencies.add( | |
| dependenciesString | |
| .substring(start, index) | |
| .replaceAll(r'\\', r'\') | |
| .replaceAll(r'\ ', ' '), | |
| ); | |
| start = index + 1; | |
| } | |
| return dependencies.where((p) => p.isNotEmpty).toList(); | |
| } | |
| /// Record the fact that the given [pluginIsolate] responded to a request with | |
| /// the given [method] in the given [time]. | |
| static void recordResponseTime( | |
| PluginIsolate pluginIsolate, | |
| String method, | |
| int time, | |
| ) { | |
| pluginResponseTimes | |
| .putIfAbsent(pluginIsolate, () => <String, PercentileCalculator>{}) | |
| .putIfAbsent(method, () => PercentileCalculator()) | |
| .addValue(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(':'); | |
| } | |
| } | |
| class _Package { | |
| final String name; | |
| final Folder root; | |
| new(this.name, this.root); | |
| } |