mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

Our current top crasher is an unclear error when ProcessManager fails to resolve an executable path. To fix this, we'd like to being adjusting the process resolution logic and adding more instrumentation to track failures. In order to begin the process, the ProcessManager has been folded back into the flutter tool
341 lines
10 KiB
Dart
341 lines
10 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
// @dart = 2.8
|
|
|
|
import 'dart:async';
|
|
import 'dart:math' as math;
|
|
|
|
import 'package:meta/meta.dart';
|
|
|
|
import '../base/common.dart';
|
|
import '../base/file_system.dart';
|
|
import '../base/io.dart';
|
|
import '../base/logger.dart';
|
|
import '../base/platform.dart';
|
|
import '../base/process.dart';
|
|
import '../base/terminal.dart';
|
|
import '../base/utils.dart';
|
|
import '../convert.dart';
|
|
|
|
/// An interface to the Dart analysis server.
|
|
class AnalysisServer {
|
|
AnalysisServer(
|
|
this.sdkPath,
|
|
this.directories, {
|
|
@required FileSystem fileSystem,
|
|
@required ProcessManager processManager,
|
|
@required Logger logger,
|
|
@required Platform platform,
|
|
@required Terminal terminal,
|
|
}) : _fileSystem = fileSystem,
|
|
_processManager = processManager,
|
|
_logger = logger,
|
|
_platform = platform,
|
|
_terminal = terminal;
|
|
|
|
final String sdkPath;
|
|
final List<String> directories;
|
|
final FileSystem _fileSystem;
|
|
final ProcessManager _processManager;
|
|
final Logger _logger;
|
|
final Platform _platform;
|
|
final Terminal _terminal;
|
|
|
|
Process _process;
|
|
final StreamController<bool> _analyzingController =
|
|
StreamController<bool>.broadcast();
|
|
final StreamController<FileAnalysisErrors> _errorsController =
|
|
StreamController<FileAnalysisErrors>.broadcast();
|
|
bool _didServerErrorOccur = false;
|
|
|
|
int _id = 0;
|
|
|
|
Future<void> start() async {
|
|
final String snapshot = _fileSystem.path.join(
|
|
sdkPath,
|
|
'bin',
|
|
'snapshots',
|
|
'analysis_server.dart.snapshot',
|
|
);
|
|
final List<String> command = <String>[
|
|
_fileSystem.path.join(sdkPath, 'bin', 'dart'),
|
|
'--disable-dart-dev',
|
|
snapshot,
|
|
'--disable-server-feature-completion',
|
|
'--disable-server-feature-search',
|
|
'--sdk',
|
|
sdkPath,
|
|
];
|
|
|
|
_logger.printTrace('dart ${command.skip(1).join(' ')}');
|
|
_process = await _processManager.start(command);
|
|
// This callback hookup can't throw.
|
|
unawaited(_process.exitCode.whenComplete(() => _process = null));
|
|
|
|
final Stream<String> errorStream = _process.stderr
|
|
.transform<String>(utf8.decoder)
|
|
.transform<String>(const LineSplitter());
|
|
errorStream.listen(_logger.printError);
|
|
|
|
final Stream<String> inStream = _process.stdout
|
|
.transform<String>(utf8.decoder)
|
|
.transform<String>(const LineSplitter());
|
|
inStream.listen(_handleServerResponse);
|
|
|
|
_sendCommand('server.setSubscriptions', <String, dynamic>{
|
|
'subscriptions': <String>['STATUS'],
|
|
});
|
|
|
|
_sendCommand('analysis.setAnalysisRoots',
|
|
<String, dynamic>{'included': directories, 'excluded': <String>[]});
|
|
}
|
|
|
|
bool get didServerErrorOccur => _didServerErrorOccur;
|
|
|
|
Stream<bool> get onAnalyzing => _analyzingController.stream;
|
|
|
|
Stream<FileAnalysisErrors> get onErrors => _errorsController.stream;
|
|
|
|
Future<int> get onExit => _process.exitCode;
|
|
|
|
void _sendCommand(String method, Map<String, dynamic> params) {
|
|
final String message = json.encode(<String, dynamic>{
|
|
'id': (++_id).toString(),
|
|
'method': method,
|
|
'params': params,
|
|
});
|
|
_process.stdin.writeln(message);
|
|
_logger.printTrace('==> $message');
|
|
}
|
|
|
|
void _handleServerResponse(String line) {
|
|
_logger.printTrace('<== $line');
|
|
|
|
final dynamic response = json.decode(line);
|
|
|
|
if (response is Map<String, dynamic>) {
|
|
if (response['event'] != null) {
|
|
final String event = response['event'] as String;
|
|
final dynamic params = response['params'];
|
|
|
|
if (params is Map<String, dynamic>) {
|
|
if (event == 'server.status') {
|
|
_handleStatus(castStringKeyedMap(response['params']));
|
|
} else if (event == 'analysis.errors') {
|
|
_handleAnalysisIssues(castStringKeyedMap(response['params']));
|
|
} else if (event == 'server.error') {
|
|
_handleServerError(castStringKeyedMap(response['params']));
|
|
}
|
|
}
|
|
} else if (response['error'] != null) {
|
|
// Fields are 'code', 'message', and 'stackTrace'.
|
|
final Map<String, dynamic> error = castStringKeyedMap(response['error']);
|
|
_logger.printError(
|
|
'Error response from the server: ${error['code']} ${error['message']}');
|
|
if (error['stackTrace'] != null) {
|
|
_logger.printError(error['stackTrace'] as String);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void _handleStatus(Map<String, dynamic> statusInfo) {
|
|
// {"event":"server.status","params":{"analysis":{"isAnalyzing":true}}}
|
|
if (statusInfo['analysis'] != null && !_analyzingController.isClosed) {
|
|
final bool isAnalyzing = statusInfo['analysis']['isAnalyzing'] as bool;
|
|
_analyzingController.add(isAnalyzing);
|
|
}
|
|
}
|
|
|
|
void _handleServerError(Map<String, dynamic> error) {
|
|
// Fields are 'isFatal', 'message', and 'stackTrace'.
|
|
_logger.printError('Error from the analysis server: ${error['message']}');
|
|
if (error['stackTrace'] != null) {
|
|
_logger.printError(error['stackTrace'] as String);
|
|
}
|
|
_didServerErrorOccur = true;
|
|
}
|
|
|
|
void _handleAnalysisIssues(Map<String, dynamic> issueInfo) {
|
|
// {"event":"analysis.errors","params":{"file":"/Users/.../lib/main.dart","errors":[]}}
|
|
final String file = issueInfo['file'] as String;
|
|
final List<dynamic> errorsList = issueInfo['errors'] as List<dynamic>;
|
|
final List<AnalysisError> errors = errorsList
|
|
.map<Map<String, dynamic>>(castStringKeyedMap)
|
|
.map<AnalysisError>((Map<String, dynamic> json) {
|
|
return AnalysisError(WrittenError.fromJson(json),
|
|
fileSystem: _fileSystem,
|
|
platform: _platform,
|
|
terminal: _terminal,
|
|
);
|
|
})
|
|
.toList();
|
|
if (!_errorsController.isClosed) {
|
|
_errorsController.add(FileAnalysisErrors(file, errors));
|
|
}
|
|
}
|
|
|
|
Future<bool> dispose() async {
|
|
await _analyzingController.close();
|
|
await _errorsController.close();
|
|
return _process?.kill();
|
|
}
|
|
}
|
|
|
|
enum AnalysisSeverity {
|
|
error,
|
|
warning,
|
|
info,
|
|
none,
|
|
}
|
|
|
|
/// [AnalysisError] with command line style.
|
|
class AnalysisError implements Comparable<AnalysisError> {
|
|
AnalysisError(
|
|
this.writtenError, {
|
|
@required Platform platform,
|
|
@required Terminal terminal,
|
|
@required FileSystem fileSystem,
|
|
}) : _platform = platform,
|
|
_terminal = terminal,
|
|
_fileSystem = fileSystem;
|
|
|
|
final WrittenError writtenError;
|
|
final Platform _platform;
|
|
final Terminal _terminal;
|
|
final FileSystem _fileSystem;
|
|
|
|
String get _separator => _platform.isWindows ? '-' : '•';
|
|
|
|
String get colorSeverity {
|
|
switch (writtenError.severityLevel) {
|
|
case AnalysisSeverity.error:
|
|
return _terminal.color(writtenError.severity, TerminalColor.red);
|
|
case AnalysisSeverity.warning:
|
|
return _terminal.color(writtenError.severity, TerminalColor.yellow);
|
|
case AnalysisSeverity.info:
|
|
case AnalysisSeverity.none:
|
|
return writtenError.severity;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
String get type => writtenError.type;
|
|
String get code => writtenError.code;
|
|
|
|
@override
|
|
int compareTo(AnalysisError other) {
|
|
// Sort in order of file path, error location, severity, and message.
|
|
if (writtenError.file != other.writtenError.file) {
|
|
return writtenError.file.compareTo(other.writtenError.file);
|
|
}
|
|
|
|
if (writtenError.offset != other.writtenError.offset) {
|
|
return writtenError.offset - other.writtenError.offset;
|
|
}
|
|
|
|
final int diff = other.writtenError.severityLevel.index -
|
|
writtenError.severityLevel.index;
|
|
if (diff != 0) {
|
|
return diff;
|
|
}
|
|
|
|
return writtenError.message.compareTo(other.writtenError.message);
|
|
}
|
|
|
|
@override
|
|
String toString() {
|
|
// Can't use "padLeft" because of ANSI color sequences in the colorized
|
|
// severity.
|
|
final String padding = ' ' * math.max(0, 7 - writtenError.severity.length);
|
|
return '$padding${colorSeverity.toLowerCase()} $_separator '
|
|
'${writtenError.messageSentenceFragment} $_separator '
|
|
'${_fileSystem.path.relative(writtenError.file)}:${writtenError.startLine}:${writtenError.startColumn} $_separator '
|
|
'$code';
|
|
}
|
|
|
|
String toLegacyString() {
|
|
return writtenError.toString();
|
|
}
|
|
}
|
|
|
|
/// [AnalysisError] in plain text content.
|
|
class WrittenError {
|
|
WrittenError._({
|
|
@required this.severity,
|
|
@required this.type,
|
|
@required this.message,
|
|
@required this.code,
|
|
@required this.file,
|
|
@required this.startLine,
|
|
@required this.startColumn,
|
|
@required this.offset,
|
|
});
|
|
|
|
/// {
|
|
/// "severity":"INFO",
|
|
/// "type":"TODO",
|
|
/// "location":{
|
|
/// "file":"/Users/.../lib/test.dart",
|
|
/// "offset":362,
|
|
/// "length":72,
|
|
/// "startLine":15,
|
|
/// "startColumn":4
|
|
/// },
|
|
/// "message":"...",
|
|
/// "hasFix":false
|
|
/// }
|
|
static WrittenError fromJson(Map<String, dynamic> json) {
|
|
return WrittenError._(
|
|
severity: json['severity'] as String,
|
|
type: json['type'] as String,
|
|
message: json['message'] as String,
|
|
code: json['code'] as String,
|
|
file: json['location']['file'] as String,
|
|
startLine: json['location']['startLine'] as int,
|
|
startColumn: json['location']['startColumn'] as int,
|
|
offset: json['location']['offset'] as int,
|
|
);
|
|
}
|
|
|
|
final String severity;
|
|
final String type;
|
|
final String message;
|
|
final String code;
|
|
|
|
final String file;
|
|
final int startLine;
|
|
final int startColumn;
|
|
final int offset;
|
|
|
|
static final Map<String, AnalysisSeverity> _severityMap = <String, AnalysisSeverity>{
|
|
'INFO': AnalysisSeverity.info,
|
|
'WARNING': AnalysisSeverity.warning,
|
|
'ERROR': AnalysisSeverity.error,
|
|
};
|
|
|
|
AnalysisSeverity get severityLevel =>
|
|
_severityMap[severity] ?? AnalysisSeverity.none;
|
|
|
|
String get messageSentenceFragment {
|
|
if (message.endsWith('.')) {
|
|
return message.substring(0, message.length - 1);
|
|
}
|
|
return message;
|
|
}
|
|
|
|
@override
|
|
String toString() {
|
|
return '[${severity.toLowerCase()}] $messageSentenceFragment ($file:$startLine:$startColumn)';
|
|
}
|
|
}
|
|
|
|
class FileAnalysisErrors {
|
|
FileAnalysisErrors(this.file, this.errors);
|
|
|
|
final String file;
|
|
final List<AnalysisError> errors;
|
|
}
|