40b4389891
Change-Id: I1b4a9394a996cdc9907273689d2c9dc0c900de55 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/142556 Reviewed-by: Paul Berry <paulberry@google.com> Commit-Queue: Janice Collins <jcollins@google.com>
165 lines
6.0 KiB
Dart
165 lines
6.0 KiB
Dart
// 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);
|
|
});
|
|
}
|
|
}
|