|  | // 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; | 
|  | }, | 
|  | ); | 
|  | } | 
|  | } |