c4d1c3a3e8
I recently introduced this deletion logic but was deleting the wrong file; this was deleting the `plugin.dart` file that `plugin.aot` is compiled from. Over the weekend I found this issue with manual testing. I've also verified this fix with manual testing. Change-Id: I3f570368068117e6bb952ada0707a09ee97b765c Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/506320 Auto-Submit: Samuel Rawlins <srawlins@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
976 lines
34 KiB
Dart
976 lines
34 KiB
Dart
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
/// @docImport 'package:analysis_server_plugin/src/plugin_server.dart';
|
|
/// @docImport 'package:analysis_server/src/plugin/plugin_watcher.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].
|
|
PluginException(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;
|
|
|
|
PluginFiles(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.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.
|
|
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].
|
|
PluginManager(
|
|
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"');
|
|
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();
|
|
|
|
pluginIsolate.addContextRoot(contextRoot);
|
|
if (startedSuccessfully) {
|
|
var analysisSetSubscriptionsParams = _analysisSetSubscriptionsParams;
|
|
if (analysisSetSubscriptionsParams != null) {
|
|
pluginIsolate.sendRequest(analysisSetSubscriptionsParams);
|
|
}
|
|
if (_overlayState.isNotEmpty) {
|
|
pluginIsolate.sendRequest(AnalysisUpdateContentParams(_overlayState));
|
|
}
|
|
var analysisSetPriorityFilesParams = _analysisSetPriorityFilesParams;
|
|
if (analysisSetPriorityFilesParams != null) {
|
|
pluginIsolate.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<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.getChildAssumingFile(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.getChildAssumingFolder(
|
|
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.getChildAssumingFolder(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 (e, st) {
|
|
instrumentationService.logException(
|
|
SilentException('Issue stopping a plugin', e, st),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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 (var 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 (var 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, {
|
|
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 (e, st) {
|
|
instrumentationService.logException(e, st);
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
|
|
/// 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".',
|
|
);
|
|
|
|
var stopwatch = Stopwatch()..start();
|
|
var depfile = entrypoint.parent.getChildAssumingFile('depfile.txt');
|
|
var aotSnapshotFile = entrypoint.parent.getChildAssumingFile('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.',
|
|
);
|
|
}
|
|
}
|
|
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}.',
|
|
);
|
|
|
|
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}'",
|
|
);
|
|
return aotSnapshotFile;
|
|
}
|
|
} catch (error, stackTrace) {
|
|
instrumentationService.logException(
|
|
'Exception while checking an existing plugin AOT snapshot: '
|
|
'"$error"\n$stackTrace',
|
|
);
|
|
}
|
|
|
|
// 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);
|
|
throw PluginException(exceptionReason);
|
|
}
|
|
|
|
return pluginFolder
|
|
.getChildAssumingFolder('bin')
|
|
.getChildAssumingFile('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
|
|
.getChildAssumingFolder('bin')
|
|
.getChildAssumingFile('plugin.dart');
|
|
if (!pluginFile.exists) {
|
|
throw PluginException("File '${pluginFile.path}' does not exist.");
|
|
}
|
|
File? packageConfigFile = pluginFolder
|
|
.getChildAssumingFolder(file_paths.dotDartTool)
|
|
.getChildAssumingFile(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.getChildAssumingFile(stateName);
|
|
if (!packageConfigFile.exists) {
|
|
var pluginPubspec = pluginFolder.getChildAssumingFile(
|
|
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.getChildAssumingFile(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
|
|
.getChildAssumingFolder('bin')
|
|
.getChildAssumingFile('plugin.aot');
|
|
if (!aotSnapshotFile.exists) return null;
|
|
var snapshotModificationStamp = aotSnapshotFile.modificationStamp;
|
|
|
|
if (pluginFile.modificationStamp > snapshotModificationStamp) return null;
|
|
var pubspecFile = pluginFolder.getChildAssumingFile(file_paths.pubspecYaml);
|
|
if (pubspecFile.modificationStamp > snapshotModificationStamp) return null;
|
|
|
|
var depfile = pluginFolder
|
|
.getChildAssumingFolder('bin')
|
|
.getChildAssumingFile('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}".',
|
|
);
|
|
|
|
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}.',
|
|
);
|
|
|
|
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;
|
|
|
|
_Package(this.name, this.root);
|
|
}
|