| // 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/session_logger/session_logger.dart'; | |
| import 'package:analysis_server/src/utilities/process.dart'; | |
| import 'package:analysis_server/src/utilities/sdk.dart'; | |
| import 'package:analyzer/instrumentation/service.dart'; | |
| import 'package:analyzer/src/util/platform_info.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; | |
| final SessionLogger _sessionLogger; | |
| final path.Context _pathContext; | |
| late final ProcessRunner _processRunner; | |
| 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(); | |
| new( | |
| this._instrumentationService, | |
| this._sessionLogger, | |
| this._pathContext, | |
| this._processRunner, | |
| ) { | |
| // 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'; | |
| var 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 { | |
| var packageDirectory = _pathContext.dirname(pubspecPath); | |
| var result = await _runPubJsonCommand([ | |
| 'outdated', | |
| '--show-all', | |
| '--json', | |
| ], workingDirectory: packageDirectory); | |
| if (result == null) { | |
| return []; | |
| } | |
| var packages = (result['packages'] as List<Object?>?) | |
| ?.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() { | |
| for (var process in _activeProcesses) { | |
| _instrumentationService.logInfo('Terminating process ${process.pid}'); | |
| _sessionLogger.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. | |
| var completer = Completer<void>(); | |
| var lastCommand = _lastQueuedCommand; | |
| _lastQueuedCommand = completer.future; | |
| // And wait for that previous command to finish. | |
| await lastCommand.catchError((_) {}); | |
| try { | |
| _instrumentationService.logInfo('Starting pub command $args'); | |
| _sessionLogger.logInfo('Starting pub command $args'); | |
| var process = await _processRunner.start( | |
| sdk.dart, | |
| ['pub', ...args], | |
| workingDirectory: workingDirectory, | |
| environment: {_pubEnvironmentKey: _pubEnvironmentValue}, | |
| ); | |
| _activeProcesses.add(process); | |
| var stdoutFuture = process.stdout.transform(utf8.decoder).join(); | |
| var stderrFuture = process.stderr.transform(utf8.decoder).join(); | |
| var exitCode = await process.exitCode; | |
| _activeProcesses.remove(process); | |
| var stdout = await stdoutFuture; | |
| var stderr = await stderrFuture; | |
| if (exitCode != 0) { | |
| _instrumentationService.logError( | |
| 'pub command returned $exitCode exit code: $stderr.', | |
| ); | |
| _sessionLogger.logException( | |
| exception: 'pub command returned $exitCode exit code: $stderr.', | |
| ); | |
| return null; | |
| } | |
| try { | |
| var results = jsonDecode(stdout); | |
| _instrumentationService.logInfo('pub command completed successfully'); | |
| _sessionLogger.logInfo('pub command completed successfully'); | |
| return results as Map<String, Object?>?; | |
| } catch (e) { | |
| _instrumentationService.logError( | |
| 'pub command returned invalid JSON: $e.', | |
| ); | |
| _sessionLogger.logException( | |
| exception: 'pub command returned invalid JSON: $e.', | |
| ); | |
| return null; | |
| } | |
| } catch (e, stackTrace) { | |
| _instrumentationService.logError('pub command failed to run: $e.'); | |
| _sessionLogger.logException( | |
| exception: 'pub command failed to run: $e.', | |
| stackTrace: stackTrace, | |
| ); | |
| return null; | |
| } finally { | |
| completer.complete(); | |
| } | |
| } | |
| String? _version(Map<String, Object?> json, String type) { | |
| var versionType = json[type] as Map<String, Object?>?; | |
| var 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; | |
| new( | |
| this.packageName, { | |
| required this.currentVersion, | |
| required this.latestVersion, | |
| required this.resolvableVersion, | |
| required this.upgradableVersion, | |
| }); | |
| } |