blob: f2bbb8c15b5b7bf6a11c236a79e0f7c48588c057 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
*
* 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 org.apache.sling.maven.kickstart.run;
import org.apache.maven.plugin.logging.Log;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.OutputStreamWriter;
import java.math.BigInteger;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.security.SecureRandom;
/**
* The <code>ControlClient</code> class is a helper class to interact with a started
* Sling instance through its ControlListener class.
*/
public class ControlClient {
// command sent by the client to cause Sling to shutdown
static final String COMMAND_STOP = "stop";
// command sent by the client to check for the status of the server
static final String COMMAND_STATUS = "status";
// command sent by the client to request a thread dump
static final String COMMAND_THREADS = "threads";
// The default interface to listen on
private static final String DEFAULT_LISTEN_INTERFACE = "127.0.0.1";
// The default port to listen on and to connect to - we select it randomly
private static final int DEFAULT_LISTEN_PORT = 0;
private String secretKey;
private InetSocketAddress socketAddress;
private File directory;
private Log logger;
/**
* Creates an instance of this control support class.
* <p>
* The host (name or address) and port number of the socket is defined by
* the <code>listenSpec</code> parameter. This parameter is defined as
* <code>[ host ":" ] port</code>. If the parameter is empty or
* <code>null</code> it defaults to <i>localhost:0</i>. If the host name
* is missing it defaults to <i>localhost</i>.
*/
public ControlClient(final File directory, Log logger) {
this.directory = directory;
this.logger = logger;
}
public int getPort() {
return socketAddress != null ? socketAddress.getPort() : -1;
}
public boolean isStarted() {
Response response = sendCommand(COMMAND_STATUS);
return response.getCode() == 0 && "OK".equals(response.getResult());
}
/**
* Implements the client side of the control connection sending the command
* to shutdown Sling.
*/
public int shutdownServer() { return sendCommand(COMMAND_STOP).getCode(); }
/**
* Implements the client side of the control connection sending the command
* to check whether Sling is active.
*/
public int statusServer() {
return sendCommand(COMMAND_STATUS).getCode();
}
/**
* Implements the client side of the control connection sending the command
* to retrieve a thread dump.
*/
public int dumpThreads() {
return sendCommand(COMMAND_THREADS).getCode();
}
/**
* Sends the given command to the server indicated by the configured
* socket address and logs the reply.
*
* @param command The command to send
*
* @return A code indicating success of sending the command.
*/
private Response sendCommand(final String command) {
if (configure()) {
if (this.secretKey == null) {
logger.info("Missing secret key to protect sending '" + command + "' to " + this.socketAddress);
return new Response(4);
}
Socket socket = null;
try {
socket = new Socket();
socket.connect(this.socketAddress);
writeLine0(socket, this.secretKey + " " + command);
final String result = readLine(socket);
logger.info("Sent '" + command + "' to " + this.socketAddress + ": " + result);
return new Response(0, result);
} catch (final ConnectException ce) {
logger.info("No Apache Sling running at " + this.socketAddress);
return new Response(3, ce);
} catch (final IOException ioe) {
logger.error("Failed sending '" + command + "' to " + this.socketAddress, ioe);
return new Response(1, ioe);
} finally {
if (socket != null) {
try {
socket.close();
} catch (IOException ignore) {
}
}
}
}
logger.info("No socket address to send '" + command + "' to");
return new Response(4);
}
private String readLine(final Socket socket) throws IOException {
final BufferedReader br = new BufferedReader(new InputStreamReader(
socket.getInputStream(), "UTF-8"));
StringBuilder b = new StringBuilder();
boolean more = true;
while (more) {
String s = br.readLine();
if (s != null && s.startsWith("-")) {
s = s.substring(1);
} else {
more = false;
}
if (b.length() > 0) {
b.append("\r\n");
}
b.append(s);
}
return b.toString();
}
private void writeLine0(final Socket socket, final String line) throws IOException {
final BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"));
bw.write(line);
bw.write("\r\n");
bw.flush();
}
/**
* Read the port from the config file
* @return The port or null
*/
private boolean configure() {
boolean result = false;
final File configFile = this.getConfigFile();
if (configFile.canRead()) {
try ( final LineNumberReader lnr = new LineNumberReader(new FileReader(configFile))) {
this.socketAddress = getSocketAddress(lnr.readLine());
this.secretKey = lnr.readLine();
result = true;
} catch (final IOException ignore) {
// ignore
}
}
return result;
}
private File getConfigFile() {
final File configDir = new File(directory, "conf");
return new File(configDir, "controlport");
}
private static String generateKey() {
return new BigInteger(165, new SecureRandom()).toString(32);
}
private InetSocketAddress getSocketAddress(String listenSpec) {
try {
final String address;
final int port;
if (listenSpec == null) {
address = DEFAULT_LISTEN_INTERFACE;
port = DEFAULT_LISTEN_PORT;
} else {
final int colon = listenSpec.indexOf(':');
if (colon < 0) {
address = DEFAULT_LISTEN_INTERFACE;
port = Integer.parseInt(listenSpec);
} else {
address = listenSpec.substring(0, colon);
port = Integer.parseInt(listenSpec.substring(colon + 1));
}
}
final InetSocketAddress addr = new InetSocketAddress(address, port);
if (!addr.isUnresolved()) {
return addr;
}
logger.error("Unknown host in '" + listenSpec);
} catch (final NumberFormatException nfe) {
logger.error("Cannot parse port number from '" + listenSpec + "'");
}
return null;
}
private static class Response {
private int code;
private String result;
private Exception exception;
public Response(int code) {
this.code = code;
}
public Response(int code, String result) {
this.code = code;
this.result = result;
}
public Response(int code, Exception exception) {
this.code = code;
this.exception = exception;
}
public int getCode() {
return code;
}
public String getResult() {
return result;
}
public Exception getException() {
return exception;
}
}
}