blob: c4ecfa218753d3dfef967db868d2759e7fce2e4c [file] [log] [blame]
// 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.
/// This library implements [ServerBase], except unlike analysis_server_client's
/// `Server`, [Server] runs the analysis server in an isolate.
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';
import 'package:analysis_server/starter.dart';
import 'package:analysis_server_client/listener/server_listener.dart';
import 'package:analysis_server_client/src/server_base.dart';
import 'package:analysis_server_client/protocol.dart';
import 'package:stream_channel/isolate_channel.dart';
/// Wrap server arguments and communication port into a single parameter
/// for [Isolate.spawn].
class _IsolateParameters {
final List<String> arguments;
final SendPort sendPort;
_IsolateParameters(this.arguments, this.sendPort);
}
/// Manage an analysis_server launched in an isolate.
class Server extends ServerBase {
/// Server isolate object, or `null` if server hasn't been started yet
/// or if the server has already been stopped.
Isolate _isolate;
/// The [ReceivePort] data subscription via an [IsolateChannel], or `null`
/// if either [listenToOutput] has not been called or [stop] has been called.
StreamSubscription<String> _receiveSubscription;
/// Construct a Server.
///
/// [isolate] and [isolateChannel] are testing-only parameters, allowing
/// you to bypass start().
Server(
{ServerListener listener,
Isolate isolate,
IsolateChannel isolateChannel,
bool stdioPassthrough = false})
: _isolate = isolate,
_isolateChannel = isolateChannel,
super(listener: listener, stdioPassthrough: stdioPassthrough);
/// Completes when the [_isolate] has exited.
Completer isolateExited = Completer();
/// The [IsolateChannel] by which this class communicates with the [_isolate].
IsolateChannel _isolateChannel;
/// Force kill the server. The returned future completes when the isolate
/// is dead.
Future<void> kill({String reason = 'none'}) {
listener?.killingServerProcess(reason);
final isolate = _isolate;
final isolateExitedOriginal = isolateExited;
_isolate = null;
isolateExited = null;
isolate.kill(priority: Isolate.immediate);
return isolateExitedOriginal.future;
}
/// Start listening to output from the server,
/// and deliver notifications to [notificationProcessor].
void listenToOutput({NotificationProcessor notificationProcessor}) {
_receiveSubscription = _isolateChannel.stream
.transform(utf8.decoder)
.transform(LineSplitter())
.listen((line) => outputProcessor(line, notificationProcessor));
}
/// 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) =>
sendCommandWith(method, params, _isolateChannel.sink.add);
/// Start the server in a new [Isolate].
Future start({
String clientId,
String clientVersion,
int diagnosticPort,
String instrumentationLogFile,
String sdkPath,
bool suppressAnalytics = true,
bool useAnalysisHighlight2 = false,
}) async {
if (_isolate != null) {
throw Exception('Isolate already started');
}
// Even though this is an isolate, most of the analysis server code
// can't tell the difference. So we construct "command line" arguments
// just the same as analysis_server_client.
List<String> arguments = [];
arguments.addAll(getServerArguments(
clientId: clientId,
clientVersion: clientVersion,
suppressAnalytics: suppressAnalytics,
diagnosticPort: diagnosticPort,
instrumentationLogFile: instrumentationLogFile,
sdkPath: sdkPath,
useAnalysisHighlight2: useAnalysisHighlight2));
listener?.startingServer('((isolate))', arguments);
ReceivePort receivePort = ReceivePort();
ReceivePort onExitReceivePort = ReceivePort();
ReceivePort onErrorReceivePort = ReceivePort();
onExitReceivePort.listen((_) {
isolateExited.complete(0);
});
onErrorReceivePort.listen((_) {
listener?.unexpectedStop(null);
});
_isolateChannel = IsolateChannel<List<int>>.connectReceive(receivePort);
_isolate = await Isolate.spawn(
_runIsolate, _IsolateParameters(arguments, receivePort.sendPort),
onExit: onExitReceivePort.sendPort);
}
/// This is the function passed to [Isolate.spawn] to actually begin
/// the server.
static void _runIsolate(_IsolateParameters parameters) {
ServerStarter starter = ServerStarter();
// TODO(jcollins-g): consider a refactor that does not require passing
// text arguments to start the server.
starter.start(parameters.arguments, parameters.sendPort);
}
/// Attempt to gracefully shutdown the server.
/// If that fails, then kill the isolate.
Future<void> stop({Duration timeLimit}) async {
timeLimit ??= const Duration(seconds: 5);
if (_isolate == null) {
// isolate already exited
return;
}
final future = send(SERVER_REQUEST_SHUTDOWN, null);
final isolate = _isolate;
_isolate = null;
await future
// fall through to wait for exit
.timeout(timeLimit, onTimeout: () {
return null;
}).whenComplete(() async {
await _receiveSubscription?.cancel();
_receiveSubscription = null;
});
return isolateExited.future.timeout(timeLimit, onTimeout: () {
listener?.killingServerProcess('server failed to exit');
isolate.kill(priority: Isolate.immediate);
});
}
}