blob: 2351441999765c45a34919860f088b7a12ea337c [file] [log] [blame]
// Copyright (c) 2021, 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:convert';
import 'dart:io';
import 'package:analysis_server/src/utilities/process.dart';
import 'package:analyzer/instrumentation/service.dart';
import 'package:path/path.dart' as path;
/// A class for interacting with the `pub` command.
///
/// `pub` commands will be queued and not run concurrently.
class PubCommand {
static const String _pubEnvironmentKey = 'PUB_ENVIRONMENT';
/// An environment variable that can be set to prevent a [PubCommand] from
/// being created/used by the analysis server.
///
/// This is generally intended for integration tests to prevent them spawning
/// pub commands while testing other functionality.
static const String disablePubCommandEnvironmentKey =
'DART_SERVER_DISABLE_PUB_COMMAND';
final InstrumentationService _instrumentationService;
late final ProcessRunner _processRunner;
late final String _pubPath;
late final String _pubEnvironmentValue;
/// Active processes that should be killed when shutting down.
final _activeProcesses = <Process>{};
/// Tracks the last queued command to avoid overlapping because pub does not
/// do its own locking when accessing the cache.
///
/// https://github.com/dart-lang/pub/issues/1178
///
/// This does not prevent running concurrently with commands spawned by other
/// tools (such as the IDE).
var _lastQueuedCommand = Future<void>.value();
PubCommand(this._instrumentationService, this._processRunner) {
_pubPath = path.join(
path.dirname(Platform.resolvedExecutable),
Platform.isWindows ? 'pub.bat' : 'pub',
);
// When calling the `pub` command, we must add an identifier to the
// PUB_ENVIRONMENT environment variable (joined with colons).
const _pubEnvString = 'analysis_server.pub_api';
final existingPubEnv = Platform.environment[_pubEnvironmentKey];
_pubEnvironmentValue = [
if (existingPubEnv?.isNotEmpty ?? false) existingPubEnv,
_pubEnvString,
].join(':');
}
/// Runs `pub outdated --show-all` and returns the results.
///
/// If any error occurs executing the command, returns an empty list.
Future<List<PubOutdatedPackageDetails>> outdatedVersions(
String pubspecPath) async {
final packageDirectory = path.dirname(pubspecPath);
final result = await _runPubJsonCommand(
['outdated', '--show-all', '--json'],
workingDirectory: packageDirectory);
if (result == null) {
return [];
}
final packages =
(result['packages'] as List<dynamic>?)?.cast<Map<String, Object?>>();
if (packages == null) {
return [];
}
return packages
.map(
(json) => PubOutdatedPackageDetails(
json['package'] as String,
currentVersion: _version(json, 'current'),
latestVersion: _version(json, 'latest'),
resolvableVersion: _version(json, 'resolvable'),
upgradableVersion: _version(json, 'upgradable'),
),
)
.toList();
}
/// Terminates any in-process commands with [ProcessSignal.sigterm].
void shutdown() {
_activeProcesses.forEach((process) {
_instrumentationService.logInfo('Terminating process ${process.pid}');
process.kill();
});
}
/// Runs a pub command and decodes JSON from `stdout`.
///
/// Returns null if:
/// - exit code is non-zero
/// - returned text cannot be decoded as JSON
Future<Map<String, Object?>?> _runPubJsonCommand(List<String> args,
{required String workingDirectory}) async {
// Atomically replace the lastQueuedCommand future with our own to ensure
// only one command waits on any previous commands future.
final completer = Completer<void>();
final lastCommand = _lastQueuedCommand;
_lastQueuedCommand = completer.future;
// And wait for that previous command to finish.
await lastCommand.catchError((_) {});
try {
final command = [_pubPath, ...args];
_instrumentationService.logInfo('Starting pub command $command');
final process = await _processRunner.start(_pubPath, args,
workingDirectory: workingDirectory,
environment: {_pubEnvironmentKey: _pubEnvironmentValue});
_activeProcesses.add(process);
final exitCode = await process.exitCode;
_activeProcesses.remove(process);
final stdout = await process.stdout.transform(utf8.decoder).join();
final stderr = await process.stderr.transform(utf8.decoder).join();
if (exitCode != 0) {
_instrumentationService
.logError('pub command returned $exitCode exit code: $stderr.');
return null;
}
try {
final results = jsonDecode(stdout);
_instrumentationService.logInfo('pub command completed successfully');
return results as Map<String, Object?>?;
} catch (e) {
_instrumentationService
.logError('pub command returned invalid JSON: $e.');
return null;
}
} catch (e) {
_instrumentationService.logError('pub command failed to run: $e.');
return null;
} finally {
completer.complete();
}
}
String? _version(Map<String, Object?> json, String type) {
final versionType = json[type] as Map<String, Object?>?;
final version =
versionType != null ? versionType['version'] as String? : null;
return version;
}
}
class PubOutdatedPackageDetails {
final String packageName;
final String? currentVersion;
final String? latestVersion;
final String? resolvableVersion;
final String? upgradableVersion;
PubOutdatedPackageDetails(
this.packageName, {
required this.currentVersion,
required this.latestVersion,
required this.resolvableVersion,
required this.upgradableVersion,
});
}