blob: 205b7c0a18821152dfba48ae3ebedc8178ce76b8 [file] [log] [blame] [edit]
// Copyright (c) 2025, 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:convert';
import 'dart:io';
import 'package:analysis_server/lsp_protocol/protocol.dart' show jsonRpcVersion;
import 'package:analysis_server/src/lsp/lsp_packet_transformer.dart';
import 'package:analysis_server/src/server/driver.dart';
import 'package:analysis_server/src/session_logger/log_entry.dart';
/// The driver used to communicate with the analysis server.
class ServerDriver {
/// The protocol being used by the server.
final ServerProtocol _protocol;
/// A list of arguments from the command-line used to start the server.
final List<String> arguments;
/// The sink used to send messages from the IDE to the server's stdin, or
/// `null` if the server has not been started using [start].
IOSink? _stdinSink;
/// The socket used to send messages from DTD to the server, or `null` if the
/// server has not been connected to DTD using [connectToDtd].
WebSocket? _dtdSocket;
/// The messages read from the analysis server's stdout.
final List<String> _messagesFromServer = [];
/// Creates a new driver that can be used to communicate with a server.
///
/// When the server is [start]ed, it will use the given [protocol].
///
/// The server is run in a separate process.
// TODO(brianwilkerson): Add a flag controlling whether the server is in the
// same process as the driver or in a separate process.
factory ServerDriver({required List<String> arguments}) {
var parsedArgs = Driver.createArgParser().parse(arguments);
var protocolOption = parsedArgs.option(Driver.serverProtocolOption);
var protocol = switch (protocolOption) {
Driver.protocolAnalyzer => ServerProtocol.legacy,
Driver.protocolLsp => ServerProtocol.lsp,
null => throw StateError('No protocol specified'),
_ => throw StateError('Unrecognized protocol $protocolOption'),
};
var useFineDependencies = parsedArgs.wasParsed(
Driver.withFineDependenciesOption,
);
return ServerDriver._(
arguments: [
'--${Driver.serverProtocolOption}=${protocol.flagValue}',
if (useFineDependencies) '--${Driver.withFineDependenciesOption}',
],
protocol: protocol,
);
}
/// Creates a new driver that can be used to communicate with a server.
///
/// When the server is [start]ed, it will use the given [protocol].
///
/// The server is run in a separate process.
ServerDriver._({required this.arguments, required ServerProtocol protocol})
: _protocol = protocol;
/// Returns the path to the `dart` executable.
String get _dartExecutable {
return Platform.resolvedExecutable;
}
/// Create a websocket through which DTD messages can be sent to the server
/// and send a request to the server asking it to connect to the socket.
// ignore: unnecessary_async
Future<void> connectToDtd() async {
// TODO(brianwilkerson): Implement this.
throw UnimplementedError();
// var socketUri = 'ws://';
// _dtdSocket = await WebSocket.connect(socketUri);
}
/// Send an exit request to the server, then close the communication channels
/// used to communicate with the server.
void exit() {
if (_protocol == ServerProtocol.lsp) {
sendMessageFromIde(
Message({
'id': 0,
'method': 'exit',
'jsonrpc': jsonRpcVersion,
'clientRequestTime': DateTime.now().millisecondsSinceEpoch,
}),
);
}
_stdinSink?.close();
_stdinSink = null;
_dtdSocket?.close();
_dtdSocket = null;
}
void sendMessageFromDTD(Message message) {
if (_dtdSocket case var socket?) {
socket.add(json.encode(message));
} else {
throw StateError(
"The method 'connectToDtd' must be invoked before "
'messages can be sent from DTD.',
);
}
}
void sendMessageFromFileWatcher(Message message) {
// TODO(brianwilkerson): Implementing this will require some additional
// support in the server. The most likely approach is to add support for a
// new protocol that will fake receiving a watch event.
}
/// Send the given [message] to the server using the communication channel
/// used by the IDE.
void sendMessageFromIde(Message message) {
if (_stdinSink case IOSink writeSink) {
var jsonEncodedBody = jsonEncode(message);
if (_protocol == ServerProtocol.lsp) {
var utf8EncodedBody = utf8.encode(jsonEncodedBody);
var header =
'Content-Length: ${utf8EncodedBody.length}\r\n'
'Content-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n';
var asciiEncodedHeader = ascii.encode(header);
// Header is always ascii, body is always utf8!
writeSink.add(asciiEncodedHeader);
writeSink.add(utf8EncodedBody);
} else {
writeSink.write(jsonEncodedBody);
}
} else {
throw StateError(
"The method 'start' must be invoked before "
'messages can be sent from the IDE.',
);
}
}
void sendMessageFromPluginIsolate(Message message) {
// TODO(brianwilkerson): Implementing this will require some additional
// support in the server. Two possibilities to consider:
// 1. Add a protocol to fake receiving a messsage from the plugin isolate.
// 2. Add a protocol to connect to a web socket as if it were a connection
// to a plugin isolate.
// The first is likely to be the easier path forward.
}
/// Send a shutdown request to the server.
void shutdown() {
if (_protocol == ServerProtocol.legacy) {
sendMessageFromIde(
Message({
'id': 0,
'method': 'server.shutdown',
'clientRequestTime': DateTime.now().millisecondsSinceEpoch,
}),
);
} else if (_protocol == ServerProtocol.lsp) {
sendMessageFromIde(
Message({
'id': 0,
'method': 'shutdown',
'jsonrpc': jsonRpcVersion,
'clientRequestTime': DateTime.now().millisecondsSinceEpoch,
}),
);
}
}
/// Create and start the server.
Future<void> start() async {
if (_stdinSink != null) {
throw StateError(
'Analysis server already started, only one active instance is allowed '
'at a time.',
);
}
var process = await Process.start(_dartExecutable, [
'language-server',
'--suppress-analytics',
...arguments,
]);
_stdinSink = process.stdin;
if (_protocol == ServerProtocol.lsp) {
process.stdout
.transform(LspPacketTransformer())
.listen(_receiveMessageFromServer);
} else {
process.stdout
.transform(const Utf8Decoder())
.transform(const LineSplitter())
.listen(_receiveMessageFromServer);
}
}
void _receiveMessageFromServer(String message) {
_messagesFromServer.add(message);
}
}
/// An indication of the protocol to be used when communicating with the server.
enum ServerProtocol {
legacy('analyzer'),
lsp('lsp');
final String flagValue;
const ServerProtocol(this.flagValue);
}