| // Copyright (c) 2020, 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_client/listener/server_listener.dart'; |
| import 'package:analysis_server_client/protocol.dart'; |
| |
| /// |
| /// Add server arguments. |
| /// |
| /// TODO(danrubel): Consider moving all cmdline argument consts |
| /// out of analysis_server and into analysis_server_client. |
| List<String> getServerArguments({ |
| String? clientId, |
| String? clientVersion, |
| int? diagnosticPort, |
| String? instrumentationLogFile, |
| String? sdkPath, |
| bool suppressAnalytics = true, |
| bool useAnalysisHighlight2 = false, |
| }) { |
| var arguments = <String>[]; |
| |
| if (clientId != null) { |
| arguments.add('--client-id'); |
| arguments.add(clientId); |
| } |
| if (clientVersion != null) { |
| arguments.add('--client-version'); |
| arguments.add(clientVersion); |
| } |
| if (suppressAnalytics) { |
| arguments.add('--suppress-analytics'); |
| } |
| if (diagnosticPort != null) { |
| arguments.add('--port'); |
| arguments.add(diagnosticPort.toString()); |
| } |
| if (instrumentationLogFile != null) { |
| arguments.add('--instrumentation-log-file=$instrumentationLogFile'); |
| } |
| if (sdkPath != null) { |
| arguments.add('--sdk=$sdkPath'); |
| } |
| if (useAnalysisHighlight2) { |
| arguments.add('--useAnalysisHighlight2'); |
| } |
| return arguments; |
| } |
| |
| /// A function via which data can be sent to a started server. |
| typedef CommandSender = void Function(List<int> utf8bytes); |
| |
| /// Type of callbacks used to process notifications. |
| typedef NotificationProcessor = void Function(Notification notification); |
| |
| /// Implementations of the class [ServerBase] manage an analysis server, |
| /// and facilitate communication to and from the server. |
| /// |
| /// Clients outside of this package may not extend or implement this class. |
| abstract class ServerBase { |
| /// Replicate all data from the server process to stdout/stderr, when true. |
| final bool _stdioPassthrough; |
| |
| /// Number which should be used to compute the 'id' |
| /// to send in the next command sent to the server. |
| int _nextId = 0; |
| |
| /// If not `null`, [_listener] will be sent information |
| /// about interactions with the server. |
| final ServerListener? _listener; |
| |
| /// Commands that have been sent to the server but not yet acknowledged, |
| /// and the [Completer] objects which should be completed |
| /// when acknowledgement is received. |
| final _pendingCommands = <String, Completer<Map<String, Object?>?>>{}; |
| |
| ServerBase({ServerListener? listener, bool stdioPassthrough = false}) |
| : _listener = listener, |
| _stdioPassthrough = stdioPassthrough; |
| |
| ServerListener? get listener => _listener; |
| |
| /// If the implementation of [ServerBase] captures an error stream, |
| /// it can use this to forward the errors to [listener] and [stderr] if |
| /// appropriate. |
| void errorProcessor( |
| String line, NotificationProcessor? notificationProcessor) { |
| if (_stdioPassthrough) stderr.writeln(line); |
| var trimmedLine = line.trim(); |
| listener?.errorMessage(trimmedLine); |
| } |
| |
| /// Force kill the server. Returns a future that completes when the server |
| /// stops. |
| Future kill({String reason = 'none'}); |
| |
| /// Start listening to output from the server, |
| /// and deliver notifications to [notificationProcessor]. |
| void listenToOutput({NotificationProcessor notificationProcessor}); |
| |
| /// Handle a (possibly) json encoded object, completing the [Completer] in |
| /// [_pendingCommands] corresponding to the response. Reports problems in |
| /// decoding or message synchronization using [listener], and replicates |
| /// raw data to [stdout] as appropriate. |
| void outputProcessor( |
| String line, NotificationProcessor? notificationProcessor) { |
| if (_stdioPassthrough) stdout.writeln(line); |
| var trimmedLine = line.trim(); |
| |
| // Guard against lines like: |
| // {"event":"server.connected","params":{...}}Observatory listening on ... |
| const observatoryMessage = 'Observatory listening on '; |
| if (trimmedLine.contains(observatoryMessage)) { |
| trimmedLine = trimmedLine |
| .substring(0, trimmedLine.indexOf(observatoryMessage)) |
| .trim(); |
| } |
| if (trimmedLine.isEmpty) { |
| return; |
| } |
| |
| listener?.messageReceived(trimmedLine); |
| Map<String, Object?> message; |
| try { |
| message = json.decoder.convert(trimmedLine); |
| } catch (exception) { |
| listener?.badMessage(trimmedLine, exception); |
| return; |
| } |
| |
| // Handle response. |
| final id = message[Response.ID]; |
| if (id != null) { |
| final completer = _pendingCommands.remove(id); |
| if (completer == null) { |
| listener?.unexpectedResponse(message, id); |
| return; |
| } |
| |
| final errorJson = message[Response.ERROR]; |
| if (errorJson != null) { |
| completer.completeError( |
| RequestError.fromJson(ResponseDecoder(null), '.error', errorJson)); |
| return; |
| } |
| |
| final resultJson = message[Response.RESULT]; |
| if (resultJson is Map<String, Object?>?) { |
| completer.complete(resultJson); |
| return; |
| } |
| |
| listener?.unexpectedResponseFormat(message); |
| return; |
| } |
| |
| // Handle notification. |
| final event = message[Notification.EVENT]; |
| if (event is String) { |
| final paramsJson = message[Notification.PARAMS]; |
| if (paramsJson is Map<String, Object?>) { |
| if (notificationProcessor != null) { |
| notificationProcessor(Notification(event, paramsJson)); |
| } |
| } else { |
| listener?.unexpectedNotificationFormat(message); |
| } |
| return; |
| } |
| |
| listener?.unexpectedMessage(message); |
| } |
| |
| /// Send a command to the server. An 'id' will be automatically assigned. |
| /// The returned [Future] will be completed when the server acknowledges |
| /// the command with a response. |
| /// If the server acknowledges the command with a normal (non-error) response, |
| /// the future will be completed with the 'result' field from the response. |
| /// If the server acknowledges the command with an error response, |
| /// the future will be completed with an error. |
| Future<Map<String, Object?>?> send( |
| String method, Map<String, Object?>? params); |
| |
| /// Encodes a request for transmission and sends it as a utf8 encoded byte |
| /// string with [sendWith]. |
| Future<Map<String, Object?>?> sendCommandWith( |
| String method, Map<String, Object?>? params, CommandSender sendWith) { |
| var id = '${_nextId++}'; |
| var command = <String, dynamic>{Request.ID: id, Request.METHOD: method}; |
| if (params != null) { |
| command[Request.PARAMS] = params; |
| } |
| final completer = Completer<Map<String, Object?>?>(); |
| _pendingCommands[id] = completer; |
| var line = json.encode(command); |
| listener?.requestSent(line); |
| sendWith(utf8.encoder.convert('$line\n')); |
| return completer.future; |
| } |
| |
| /// Start the server. The returned future completes when the server |
| /// is started and it is valid to call [listenToOutput]. |
| Future start({ |
| String clientId, |
| String clientVersion, |
| int diagnosticPort, |
| String instrumentationLogFile, |
| String sdkPath, |
| bool suppressAnalytics, |
| bool useAnalysisHighlight2, |
| }); |
| |
| /// Attempt to gracefully shutdown the server. |
| /// If that fails, then force it to shut down. |
| Future stop({Duration timeLimit}); |
| } |