| // 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:async'; | |
| 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; | |
| /// Stream controller for analysis server output messages. | |
| final StreamController<Message> _serverMessagesController = | |
| StreamController(); | |
| /// 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({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. | |
| new _({required this.arguments, required this._protocol}); | |
| /// The messages read from the analysis server's stdout. | |
| Stream<Message> get serverMessages => _serverMessagesController.stream; | |
| /// 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; | |
| _serverMessagesController.close(); | |
| } | |
| 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) { | |
| if (_serverMessagesController.isClosed) { | |
| stderr.writeln('Got analysis server message after shutdown:\n$message'); | |
| } else { | |
| _serverMessagesController.add(jsonDecode(message) as Message); | |
| } | |
| } | |
| } | |
| /// An indication of the protocol to be used when communicating with the server. | |
| enum ServerProtocol { | |
| legacy('analyzer'), | |
| lsp('lsp'); | |
| final String flagValue; | |
| new(this.flagValue); | |
| } |