// 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/src/channel/channel.dart'; import 'package:analysis_server/src/channel/web_socket_channel.dart'; import 'package:analysis_server/src/protocol.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 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 _launchServer(String pathToServer) { // TODO dynamically allocate port and/or allow client to specify port List 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 _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 _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 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 start(String serverPath) { return new AnalysisManager()._launchServer(serverPath); } }