blob: 125c60603d7b3a8e96e8fa3172366e8a5e29e8b2 [file] [log] [blame]
/*
* Copyright (c) 2014, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.eclipse.org/legal/epl-v10.html
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.dart.server.internal.remote;
import com.google.common.base.Preconditions;
import com.google.dart.server.AnalysisServerSocket;
import com.google.dart.server.utilities.general.StringUtilities;
import com.google.dart.server.utilities.logging.Logging;
import java.io.IOException;
import java.net.ServerSocket;
import java.util.ArrayList;
import java.util.List;
/**
* A remote server socket over standard input and output.
*
* @coverage dart.server.remote
*/
public class StdioServerSocket implements AnalysisServerSocket {
/**
* Find and return an unused server socket port.
*/
public static int findUnusedPort() {
try {
ServerSocket ss = new ServerSocket(0);
int port = ss.getLocalPort();
ss.close();
return port;
} catch (IOException ioe) {
//$FALL-THROUGH$
}
return -1;
}
private final String runtimePath;
private final String analysisServerPath;
private final DebugPrintStream debugStream;
private final boolean debugRemoteProcess;
private final boolean profileRemoteProcess;
private int httpPort;
private RequestSink requestSink;
private ResponseStream responseStream;
private ByteLineReaderStream errorStream;
private Process process;
private final String[] additionalProgramArguments;
/**
* If non-null, the package root that should be provided to Dart when running the analysis server.
*/
private final String packageRoot;
/**
* Boolean used to have the {@code --no-error-notification} which disables all error notifications
* from the server.
* <p>
* This should be {@code null} if the snapshot path is passed as the server path.
*/
private final boolean noErrorNotification;
/**
* An option of the ways files can be read from disk, some clients normalize end of line
* characters which would make the file offset and range information incorrect.
*/
private final FileReadMode fileReadMode;
/**
* The identifier used to identify this client to the server, or {@code null} if the client does
* not choose to identify itself.
*/
private String clientId;
/**
* The identifier used to identify this client to the server, or {@code null} if the client does
* not choose to identify itself.
*/
private String clientVersion;
public StdioServerSocket(String runtimePath, String analysisServerPath,
DebugPrintStream debugStream, boolean debugRemoteProcess, boolean profileRemoteProcess,
int httpPort) {
this(
runtimePath,
analysisServerPath,
StringUtilities.EMPTY,
debugStream,
StringUtilities.EMPTY_ARRAY,
debugRemoteProcess,
profileRemoteProcess,
httpPort,
false,
FileReadMode.AS_IS);
}
public StdioServerSocket(String runtimePath, String analysisServerPath, String packageRoot,
DebugPrintStream debugStream, String[] additionalProgramArguments,
boolean debugRemoteProcess, boolean profileRemoteProcess, int httpPort,
boolean noErrorNotification, FileReadMode fileReadMode) {
this.runtimePath = runtimePath;
this.analysisServerPath = analysisServerPath;
this.packageRoot = packageRoot;
this.debugStream = debugStream;
this.additionalProgramArguments = additionalProgramArguments;
this.debugRemoteProcess = debugRemoteProcess;
this.profileRemoteProcess = profileRemoteProcess;
this.httpPort = httpPort;
this.noErrorNotification = noErrorNotification;
this.fileReadMode = fileReadMode;
}
@Override
public ByteLineReaderStream getErrorStream() {
Preconditions.checkNotNull(errorStream, "Server is not started.");
return errorStream;
}
@Override
public RequestSink getRequestSink() {
Preconditions.checkNotNull(requestSink, "Server is not started.");
return requestSink;
}
@Override
public ResponseStream getResponseStream() {
Preconditions.checkNotNull(responseStream, "Server is not started.");
return responseStream;
}
@Override
public boolean isOpen() {
try {
if (process != null) {
process.exitValue();
}
return false;
} catch (IllegalThreadStateException ex) {
return true;
}
}
/**
* Set the identifier used to identify this client to the server to the given identifier. The
* identifier must be set before the server has been started.
*/
public void setClientId(String id) {
clientId = id;
}
/**
* Set the identifier used to identify this client version to the server to the given identifier.
* The identifier must be set before the server has been started.
*/
public void setClientVersion(String version) {
clientVersion = version;
}
@Override
public void start() throws Exception {
int debugPort = findUnusedPort();
String[] arguments = computeProcessArguments(debugPort);
if (debugStream != null) {
StringBuilder builder = new StringBuilder();
builder.append(" ");
int count = arguments.length;
for (int i = 0; i < count; i++) {
if (i > 0) {
builder.append(' ');
}
builder.append(arguments[i]);
}
debugStream.println(System.currentTimeMillis() + " started analysis server:");
debugStream.println(builder.toString());
}
ProcessBuilder processBuilder = new ProcessBuilder(arguments);
process = processBuilder.start();
requestSink = new ByteRequestSink(process.getOutputStream(), debugStream);
responseStream = new ByteResponseStream(process.getInputStream(), debugStream);
errorStream = new ByteLineReaderStream(process.getErrorStream());
if (debugRemoteProcess) {
Logging.getLogger().logInformation("Analysis server debug port " + debugPort);
}
if (httpPort != 0) {
Logging.getLogger().logInformation("Analysis server http port " + httpPort);
}
}
/**
* Wait up to 5 seconds for process to gracefully exit, then forcibly terminate the process if it
* is still running.
*/
@Override
public void stop() {
if (process == null) {
return;
}
final Process processToStop = process;
process = null;
long endTime = System.currentTimeMillis() + 5000;
while (System.currentTimeMillis() < endTime) {
try {
int exit = processToStop.exitValue();
if (exit != 0) {
Logging.getLogger().logInformation(
"Non-zero exit code: " + exit + " for\n " + analysisServerPath);
}
return;
} catch (IllegalThreadStateException e) {
//$FALL-THROUGH$
}
try {
Thread.sleep(20);
} catch (InterruptedException e) {
//$FALL-THROUGH$
}
}
processToStop.destroy();
Logging.getLogger().logInformation("Terminated " + analysisServerPath);
}
/**
* Compute and return the command-line arguments used to start the analysis server process.
*
* @param debugPort the port that the VM should use for debug connections
* @return the command-line arguments that were computed
*/
private String[] computeProcessArguments(int debugPort) {
List<String> args = new ArrayList<String>();
//
// The path to the VM.
//
args.add(runtimePath);
//
// VM arguments.
//
if (packageRoot != null) {
args.add("--package-root=" + packageRoot);
}
if (debugRemoteProcess) {
args.add("--debug:" + debugPort);
}
if (profileRemoteProcess) {
args.add("--observe");
args.add("--pause-isolates-on-exit");
args.add("--code_comments");
args.add("--profile_depth=16");
args.add("--collect-code=false");
}
//
// The analysis server path.
//
args.add(analysisServerPath);
//
// Analysis server arguments.
//
if (clientId != null) {
args.add("--client-id=" + clientId);
}
if (clientVersion != null) {
args.add("--client-version=" + clientVersion);
}
if (httpPort != 0) {
args.add("--port=" + httpPort);
}
if (noErrorNotification) {
args.add("--no-error-notification");
}
if (fileReadMode == FileReadMode.NORMALIZE_EOL_ALWAYS) {
args.add("--file-read-mode=normalize-eol-always");
}
for (String arg : additionalProgramArguments) {
args.add(arg);
}
return args.toArray(new String[args.size()]);
}
}