| // Copyright (c) 2018, 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'; |
| import 'package:path/path.dart'; |
| |
| /// Type of callbacks used to process notifications. |
| typedef void NotificationProcessor(Notification notification); |
| |
| /// Instances of the class [Server] manage a server process, |
| /// and facilitate communication to and from the server. |
| /// |
| /// Clients may not extend, implement or mix-in this class. |
| class Server { |
| /// If not `null`, [_listener] will be sent information |
| /// about interactions with the server. |
| ServerListener _listener; |
| |
| /// Server process object, or `null` if server hasn't been started yet |
| /// or if the server has already been stopped. |
| Process _process; |
| |
| /// 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, dynamic>>>{}; |
| |
| /// Number which should be used to compute the 'id' |
| /// to send in the next command sent to the server. |
| int _nextId = 0; |
| |
| /// The stderr subscription or `null` if either |
| /// [listenToOutput] has not been called or [stop] has been called. |
| StreamSubscription<String> _stderrSubscription; |
| |
| /// The stdout subscription or `null` if either |
| /// [listenToOutput] has not been called or [stop] has been called. |
| StreamSubscription<String> _stdoutSubscription; |
| |
| Server({ServerListener listener, Process process}) |
| : this._listener = listener, |
| this._process = process; |
| |
| /// Force kill the server. Returns exit code future. |
| Future<int> kill({String reason = 'none'}) { |
| _listener?.killingServerProcess(reason); |
| final process = _process; |
| _process = null; |
| process.kill(); |
| return process.exitCode; |
| } |
| |
| /// Start listening to output from the server, |
| /// and deliver notifications to [notificationProcessor]. |
| void listenToOutput({NotificationProcessor notificationProcessor}) { |
| _stdoutSubscription = _process.stdout |
| .transform(utf8.decoder) |
| .transform(new LineSplitter()) |
| .listen((String line) { |
| String 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, dynamic> message; |
| try { |
| message = json.decoder.convert(trimmedLine); |
| } catch (exception) { |
| _listener?.badMessage(trimmedLine, exception); |
| return; |
| } |
| |
| final id = message[Response.ID]; |
| if (id != null) { |
| // Handle response |
| final completer = _pendingCommands.remove(id); |
| if (completer == null) { |
| _listener?.unexpectedResponse(message, id); |
| } |
| if (message.containsKey(Response.ERROR)) { |
| completer.completeError(new RequestError.fromJson( |
| new ResponseDecoder(null), '.error', message[Response.ERROR])); |
| } else { |
| completer.complete(message[Response.RESULT]); |
| } |
| } else { |
| // Handle notification |
| final String event = message[Notification.EVENT]; |
| if (event != null) { |
| if (notificationProcessor != null) { |
| notificationProcessor( |
| new Notification(event, message[Notification.PARAMS])); |
| } |
| } else { |
| _listener?.unexpectedMessage(message); |
| } |
| } |
| }); |
| _stderrSubscription = _process.stderr |
| .transform(utf8.decoder) |
| .transform(new LineSplitter()) |
| .listen((String line) { |
| String trimmedLine = line.trim(); |
| _listener?.errorMessage(trimmedLine); |
| }); |
| } |
| |
| /// 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, dynamic>> send( |
| String method, Map<String, dynamic> params) { |
| String id = '${_nextId++}'; |
| Map<String, dynamic> command = <String, dynamic>{ |
| Request.ID: id, |
| Request.METHOD: method |
| }; |
| if (params != null) { |
| command[Request.PARAMS] = params; |
| } |
| final completer = new Completer<Map<String, dynamic>>(); |
| _pendingCommands[id] = completer; |
| String line = json.encode(command); |
| _listener?.requestSent(line); |
| _process.stdin.add(utf8.encoder.convert("$line\n")); |
| return completer.future; |
| } |
| |
| /// Start the server. |
| /// |
| /// If [profileServer] is `true`, the server will be started |
| /// with "--observe" and "--pause-isolates-on-exit", allowing the observatory |
| /// to be used. |
| /// |
| /// If [serverPath] is specified, then that analysis server will be launched, |
| /// otherwise the analysis server snapshot in the SDK will be launched. |
| Future start({ |
| String clientId, |
| String clientVersion, |
| int diagnosticPort, |
| String instrumentationLogFile, |
| bool profileServer: false, |
| String sdkPath, |
| String serverPath, |
| int servicesPort, |
| bool suppressAnalytics: true, |
| bool useAnalysisHighlight2: false, |
| }) async { |
| if (_process != null) { |
| throw new Exception('Process already started'); |
| } |
| String dartBinary = Platform.executable; |
| |
| // The integration tests run 3x faster when run from snapshots |
| // (you need to run test.py with --use-sdk). |
| if (serverPath == null) { |
| // Look for snapshots/analysis_server.dart.snapshot. |
| serverPath = normalize(join(dirname(Platform.resolvedExecutable), |
| 'snapshots', 'analysis_server.dart.snapshot')); |
| |
| if (!FileSystemEntity.isFileSync(serverPath)) { |
| // Look for dart-sdk/bin/snapshots/analysis_server.dart.snapshot. |
| serverPath = normalize(join(dirname(Platform.resolvedExecutable), |
| 'dart-sdk', 'bin', 'snapshots', 'analysis_server.dart.snapshot')); |
| } |
| } |
| |
| List<String> arguments = []; |
| // |
| // Add VM arguments. |
| // |
| if (profileServer) { |
| if (servicesPort == null) { |
| arguments.add('--observe'); |
| } else { |
| arguments.add('--observe=$servicesPort'); |
| } |
| arguments.add('--pause-isolates-on-exit'); |
| } else if (servicesPort != null) { |
| arguments.add('--enable-vm-service=$servicesPort'); |
| } |
| if (Platform.packageConfig != null) { |
| arguments.add('--packages=${Platform.packageConfig}'); |
| } |
| // |
| // Add the server executable. |
| // |
| arguments.add(serverPath); |
| // |
| // Add server arguments. |
| // |
| // TODO(danrubel): Consider moving all cmdline argument consts |
| // out of analysis_server and into analysis_server_client |
| 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'); |
| } |
| _listener?.startingServer(dartBinary, arguments); |
| _process = await Process.start(dartBinary, arguments); |
| _process.exitCode.then((int code) { |
| if (code != 0 && _process != null) { |
| // Report an error if server abruptly terminated |
| _listener?.unexpectedStop(code); |
| } |
| }); |
| } |
| |
| /// Attempt to gracefully shutdown the server. |
| /// If that fails, then kill the process. |
| Future<int> stop({Duration timeLimit}) async { |
| timeLimit ??= const Duration(seconds: 5); |
| if (_process == null) { |
| // Process already exited |
| return -1; |
| } |
| final future = send(SERVER_REQUEST_SHUTDOWN, null); |
| final process = _process; |
| _process = null; |
| await future |
| // fall through to wait for exit |
| .timeout(timeLimit, onTimeout: () { |
| return null; |
| }).whenComplete(() async { |
| await _stderrSubscription?.cancel(); |
| _stderrSubscription = null; |
| await _stdoutSubscription?.cancel(); |
| _stdoutSubscription = null; |
| }); |
| return process.exitCode.timeout( |
| timeLimit, |
| onTimeout: () { |
| _listener?.killingServerProcess('server failed to exit'); |
| process.kill(); |
| return process.exitCode; |
| }, |
| ); |
| } |
| } |