blob: fd9bfcd24a6c449403a27fe3542f5b7559a53c4d [file] [log] [blame]
// Copyright (c) 2014, 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/plugin/protocol/protocol.dart';
import 'package:analysis_server/src/channel/channel.dart';
import 'package:analysis_server/src/channel/web_socket_channel.dart';
/**
* [AnalysisManager] is used to launch and manage an analysis server
* running in a separate process using either the [start] or [connect] methods.
*/
class AnalysisManager {
// TODO dynamically allocate port and/or allow client to specify port
static const int PORT = 3333;
/**
* The analysis server process being managed
* or `null` if managing an analysis server that was already running.
*/
Process process;
/**
* The channel used to communicate with the analysis server.
*/
ClientCommunicationChannel channel;
/**
* Stop the analysis server.
*
* Returns `true` if the signal is successfully sent and process terminates.
* Otherwise there was no attached process or the signal could not be sent,
* usually meaning that the process is already dead.
*/
Future<bool> stop() {
if (process == null) {
return channel.close().then((_) => false);
}
return channel
.sendRequest(new ServerShutdownParams().toRequest('0'))
.timeout(new Duration(seconds: 2), onTimeout: () {
print('Expected shutdown response');
}).then((Response response) {
return channel.close().then((_) => process.exitCode);
}).timeout(new Duration(seconds: 2), onTimeout: () {
print('Expected server to shutdown');
process.kill();
}).then((int result) {
if (result != null && result != 0) {
exitCode = result;
}
return true;
});
}
/**
* Launch an analysis server and open a connection to that server.
*/
Future<AnalysisManager> _launchServer(String pathToServer) {
// TODO dynamically allocate port and/or allow client to specify port
List<String> serverArgs = [pathToServer, '--port', PORT.toString()];
return Process.start(Platform.executable, serverArgs).catchError((error) {
exitCode = 1;
throw 'Failed to launch analysis server: $error';
}).then(_listenForPort);
}
/**
* Listen for a port from the given analysis server process.
*/
Future<AnalysisManager> _listenForPort(Process process) {
this.process = process;
// Echo stdout and stderr
Stream out = process.stdout.transform(UTF8.decoder).asBroadcastStream();
out.listen((line) => print(line));
process.stderr.pipe(stderr);
// Listen for port from server
const String pattern = 'Listening on port ';
return out
.firstWhere((String line) => line.startsWith(pattern))
.timeout(new Duration(seconds: 10))
.catchError((error) {
exitCode = 1;
process.kill();
throw 'Expected port from analysis server';
}).then((String line) {
String port = line.substring(pattern.length).trim();
String url = 'ws://${InternetAddress.LOOPBACK_IP_V4.address}:$port/';
return _openConnection(url);
});
}
/**
* Open a connection to the analysis server using the given URL.
*/
Future<AnalysisManager> _openConnection(String serverUrl) {
Function onError = (error) {
exitCode = 1;
if (process != null) {
process.kill();
}
throw 'Failed to connect to analysis server at $serverUrl\n $error';
};
try {
return WebSocket
.connect(serverUrl)
.catchError(onError)
.then((WebSocket socket) {
this.channel = new WebSocketClientChannel(socket);
return this;
});
} catch (error) {
onError(error);
}
}
/**
* Open a connection to a running analysis server
* and return a future with a manager for that analysis server.
*/
static Future<AnalysisManager> connect(String serverUrl) {
return new AnalysisManager()._openConnection(serverUrl);
}
/**
* Launch analysis server in a separate process
* and return a future with a manager for that analysis server.
*/
static Future<AnalysisManager> start(String serverPath) {
return new AnalysisManager()._launchServer(serverPath);
}
}