blob: d7865f2c3a7c8faf2efaee2cdafe2f8000b5a744 [file]
// 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 'package:analysis_server/src/analysis_server.dart';
import 'package:analysis_server/src/plugin/plugin_isolate.dart';
import 'package:analysis_server/src/plugin/plugin_manager.dart';
import 'package:analysis_server/src/status/diagnostics.dart';
import 'package:analysis_server/src/status/utilities/string_extensions.dart';
import 'package:analyzer/src/analysis_options/analysis_options.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:path/path.dart' as path;
/// The page that displays information about _new_ (not legacy) analyzer
/// plugins.
class PluginsPage extends DiagnosticPageWithNav {
@override
AnalysisServer server;
new(DiagnosticsSite site, this.server)
: super(site, 'plugins', 'Plugins', description: 'Plugins in use.');
@override
Future<void> generateContent(Map<String, String> params) async {
// Maps each plugin name to the list of configurations where that plugin is
// configured.
var allConfigs = <String, List<_ConfigurationOrigin>>{};
for (var driver in server.driverMap.values) {
var uniqueOptionsSet = driver.analysisOptionsMap.options.toSet();
for (var options in uniqueOptionsSet) {
if (options.file case var file?) {
for (var config in options.pluginConfigurations) {
var origin = _ConfigurationOrigin(
optionsPath: file.path,
config: config,
);
allConfigs.putIfAbsent(config.name, () => []).add(origin);
}
}
}
}
_emitIncompatibleInfo();
var pluginIsolates = server.pluginManager.newPluginIsolates;
if (pluginIsolates.isEmpty) {
if (allConfigs.isEmpty) {
blankslate('No known analysis plugins.');
} else {
h3('Warning: no running isolates');
p(
'Plugins have been specified in the analysis options files listed '
'below, but no plugin isolates are currently running. This may '
'indicate a startup failure or that the plugin system is disabled.',
);
h3('Configured plugins');
for (var MapEntry(key: pluginName, value: specs)
in allConfigs.entries) {
p('<strong>$pluginName</strong> specified in:');
ul(specs, (spec) {
buf.writeln('<code>${spec.optionsPath}</code>');
});
}
}
return;
}
pluginIsolates.sort(
(first, second) => first.pluginId.compareTo(second.pluginId),
);
for (var isolate in pluginIsolates) {
await _emitIsolateInfo(isolate);
_emitPerformance(isolate);
}
}
void _emitIncompatibleInfo() {
var incompatibleSources = <String, List<String>>{};
for (var driver in server.driverMap.values) {
var contextPath = driver.analysisContext?.contextRoot.root.path ?? '';
var uniqueOptionsSet = driver.analysisOptionsMap.options.toSet();
var driverConfigs = <String, List<_ConfigurationOrigin>>{};
for (var options in uniqueOptionsSet) {
if (options.file case var file?) {
for (var config in options.pluginConfigurations) {
var origin = _ConfigurationOrigin(
optionsPath: file.path,
config: config,
);
driverConfigs.putIfAbsent(config.name, () => []).add(origin);
}
}
}
for (var MapEntry(key: pluginName, value: specs)
in driverConfigs.entries) {
if (specs.length < 2) continue;
for (int i = 0; i < specs.length; i++) {
for (int j = i + 1; j < specs.length; j++) {
var spec1 = specs[i];
var spec2 = specs[j];
if (spec1.config.source != spec2.config.source) {
var spec1Yaml = spec1.config.sourceYaml().trim();
var spec2Yaml = spec2.config.sourceYaml().trim();
incompatibleSources
.putIfAbsent(pluginName, () => [])
.add(
'In context <code>$contextPath</code>: '
'<code>${spec1.optionsPath}</code> specifies '
'<pre><code>$spec1Yaml</code></pre>'
'but <code>${spec2.optionsPath}</code> specifies '
'<pre><code>$spec2Yaml</code></pre>'
'The two plugin specifications must be identical.',
);
}
}
}
}
}
if (incompatibleSources.isNotEmpty) {
h3('Incompatible plugin sources');
p(
'The following plugins are specified with incompatible sources (different '
'version constraints, paths, or git configurations) in different analysis '
'options files within the same context. This prevents them from being '
'run in the same isolate, and forces the server to spawn multiple '
'isolates.',
);
for (var MapEntry(key: pluginName, value: conflictList)
in incompatibleSources.entries) {
p('<strong>$pluginName</strong>:');
ul(conflictList, (conflict) {
buf.writeln(conflict);
});
}
}
}
Future<void> _emitIsolateInfo(PluginIsolate isolate) async {
p('The following plugins are running from a single bootstrapped location:');
_emitTable([
['Bootstrap package path:', isolate.pluginId],
if (isolate.executionPath case var executionPath?)
['Execution path:', executionPath.wordBreakOnSlashes],
if (isolate.packageConfigPath case var packageConfigPath?)
[
'Package config path:',
'${path.posix.dirname(packageConfigPath)}/'.wordBreakOnSlashes +
formatContentsLink(
packageConfigPath,
file_paths.packageConfigJson,
),
],
]);
var otherIsolates = <PluginIsolate>[];
for (var contextRoot in isolate.contextRoots) {
for (var otherIsolate in server.pluginManager.newPluginIsolates) {
if (otherIsolate != isolate &&
otherIsolate.contextRoots.contains(contextRoot)) {
if (!otherIsolates.contains(otherIsolate)) {
otherIsolates.add(otherIsolate);
}
}
}
}
if (otherIsolates.isNotEmpty) {
p(
'Note: This isolate is analyzing a context that is also analyzed by '
'other plugin isolates. Plugins are separated into different isolates '
'to ensure that pub can consistently solve their version constraints.',
);
p('Other isolates analyzing the same context:');
ul(otherIsolates, (other) {
buf.writeln('<code>${other.pluginId}</code>');
});
}
if (isolate.data.name == null) {
// This indicates the isolate is not actually running.
if (isolate.exception != null) {
p('Not running due to:');
pre(() {
buf.write(isolate.exception);
});
} else {
p(
'Not running for unknown reason (no exception was caught while '
'starting).',
);
}
return;
}
h3('Associated contexts:');
var contexts = isolate.contextRoots;
if (contexts.isEmpty) {
blankslate('none');
} else {
ul(contexts.toList(), (root) {
buf.writeln(root.root);
});
}
var details = await isolate.requestDetails();
if (details == null) {
// Either the plugin is not alive, or the plugin did not respond in time.
// TODO(srawlins): Distinguish between the two.
p('Plugin isolate did not respond with plugin details.');
return;
}
for (var plugin in details.plugins) {
h3(plugin.name);
var prints = isolate.pluginPrints[plugin.name];
if (prints != null) {
h3('Debug print output:');
pre(() {
var discardedPrintCount =
isolate.discardedPluginPrintCount[plugin.name];
if (discardedPrintCount != null && discardedPrintCount > 0) {
buf.write('(... $discardedPrintCount previous messages)');
}
for (var print in prints) {
var timestamp = DateTime.fromMillisecondsSinceEpoch(
print.timestamp,
);
buf.write('$timestamp: ${print.message}\n');
}
});
}
if (plugin.lintRules.isNotEmpty) {
p('Lint rules:');
ul(plugin.lintRules, (rule) {
buf.writeln(rule);
});
}
if (plugin.warningRules.isNotEmpty) {
p('Warning rules:');
ul(plugin.warningRules, (rule) {
buf.writeln(rule);
});
}
if (plugin.fixes.isNotEmpty) {
p('Quick fixes:');
ul(plugin.fixes, (fix) {
var codes = fix.codes.join(', ');
buf.writeln('${fix.id}: "${fix.message}" to fix $codes');
});
}
if (plugin.assists.isNotEmpty) {
p('Assists:');
ul(plugin.assists, (assist) {
buf.writeln('${assist.id}: "${assist.message}"');
});
}
}
}
void _emitPerformance(PluginIsolate isolate) {
h3('Performance:');
p('''
The server communicates with the plugins by sending requests. Each line
below provides aggregated information about one of the requests and begins with
the name of the request. The 'count' is the number of times the request was
sent. The percentiles are the 50th, 75th, 90th, 95th, and 100th percentiles of
the number of milliseconds it took to receiver a response.
''');
var responseTimes = PluginManager.pluginResponseTimes[isolate] ?? {};
var entries = responseTimes.entries.toList();
entries.sort((first, second) => first.key.compareTo(second.key));
for (var entry in entries) {
var requestName = entry.key;
var data = entry.value;
// TODO(brianwilkerson): Consider displaying these times as a graph,
// similar to the one in CompletionPage.generateContent.
var buffer = StringBuffer();
buffer.write(requestName);
buffer.write(' ');
buffer.write(data.toAnalyticsString());
p(buffer.toString());
}
}
void _emitTable(List<List<String>> data) {
buf.writeln('<table>');
for (var row in data) {
buf.writeln('<tr>');
for (var value in row) {
buf.writeln('<td>$value</td>');
}
buf.writeln('</tr>');
}
buf.writeln('</table>');
}
}
/// Represents the location and configuration of a plugin specified in an
/// analysis options file, used for diagnostic reporting on the plugins page.
class _ConfigurationOrigin({
required final String optionsPath,
required final PluginConfiguration config,
});