/*
* Copyright (C) 2006-2016 DLR, Germany
*
* All rights reserved
*
* http://www.rcenvironment.de/
*/
package de.rcenvironment.core.shutdown;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import de.rcenvironment.core.configuration.bootstrap.BootstrapConfiguration;
import de.rcenvironment.core.start.common.Instance;
import de.rcenvironment.core.toolkitbridge.api.ToolkitBridge;
import de.rcenvironment.core.toolkitbridge.transitional.ConcurrencyUtils;
import de.rcenvironment.core.utils.common.StringUtils;
import de.rcenvironment.toolkit.modules.concurrency.api.TaskDescription;
import de.rcenvironment.toolkit.utils.common.IdGenerator;
/**
* This class either (a) opens a tcp port and listens for orders to shutdown itself or (b) it tries to connect to another running instance
* of rce and send the order to shut down before it shuts down itself. The argument --shutdown triggers (b). To prevent unintended
* shutdowns, the port is chosen randomly and a secret token, stored in the configurations folder is checked.
*
* @author Oliver Seebach
* @author Robert Mischke
*/
public class HeadlessShutdown {
private static final int SHUTDOWN_TOKEN_LENGTH = 32;
private static final String TOKEN_FILENAME = "shutdown.dat";
private static final String HOST = "localhost";
private static final int REGULAR_SHUTDOWN_WAIT_TIME_MSEC = 20000; // in ms
private static final int BUFFERSIZE = 200;
private BootstrapConfiguration bootstrapSettings;
private final Log logger = LogFactory.getLog(getClass());
/**
* Triggers either the initiation of the shutdown mechanism either is client or server.
*
* @param bootstrapConfiguration the global BootstrapConfiguration instance
*/
public void executeByLaunchConfiguration(BootstrapConfiguration bootstrapConfiguration) {
this.bootstrapSettings = bootstrapConfiguration;
if (bootstrapSettings.isShutdownRequested()) {
try {
writeToLog("Running this instance as a shutdown signal sender");
sendShutdownTokenInternal(bootstrapSettings.getTargetShutdownDataDirectory());
} catch (IOException e) {
logger.error("Failed to shutdown external instance: " + e.getMessage());
throw new RuntimeException(e);
} finally {
tryToRemoveInternalProfileDir();
System.exit(0);
}
} else {
try {
initReceiver(bootstrapSettings.getOwnShutdownDataDirectory());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
/**
* Sends a shutdown signal to a different (external) instance.
*
* @param externalProfileDir the profile directory of the external instance
* @throws IOException if sending the shutdown signal fails
*/
public void shutdownExternalInstance(File externalProfileDir) throws IOException {
sendShutdownTokenInternal(new File(externalProfileDir, BootstrapConfiguration.PROFILE_SHUTDOWN_DATA_SUBDIR));
}
private void writeToLog(String text) {
logger.debug(text);
}
// RECEIVER PART
private void initReceiver(File shutdownDataDir) throws IOException {
if (!shutdownDataDir.exists()) {
shutdownDataDir.mkdirs();
}
// Automatically create socket on free port
final ServerSocket serverSocket = new ServerSocket(0);
int port = serverSocket.getLocalPort();
// generate (pseudo-)random secret token
final String secretString = IdGenerator.secureRandomHexString(SHUTDOWN_TOKEN_LENGTH);
String secret = StringUtils.escapeAndConcat(String.valueOf(port), secretString);
File secretFile = new File(shutdownDataDir, TOKEN_FILENAME);
FileUtils.writeStringToFile(secretFile, secret);
secretFile.deleteOnExit();
writeToLog("Stored shutdown information at location " + secretFile.getAbsolutePath());
// necessary as both bundles are on OSGi start level 3, and the wrapped method uses ConcurrencyUtils.getAsyncTaskService()
ToolkitBridge.afterToolkitAvailable(new Runnable() {
@Override
public void run() {
startShutdownListener(serverSocket, secretString);
}
});
}
private void startShutdownListener(final ServerSocket serverSocket, final String secretString) {
ConcurrencyUtils.getAsyncTaskService().execute(new Runnable() {
@Override
@TaskDescription("Service/daemon shutdown listener")
public void run() {
writeToLog("Listening for shutdown signals");
Socket client = null;
String message = null;
try {
client = waitForConnection(serverSocket);
message = readMessage(client);
} catch (IOException e1) {
e1.printStackTrace();
}
if (message != null) {
writeToLog("Message \"" + message + "\" received");
if (message.contains("shutdown") && message.contains(secretString)) {
writeToLog("Received shutdown signal, shutting down");
IOUtils.closeQuietly(serverSocket);
Instance.shutdown(); // non-blocking
try {
Thread.sleep(REGULAR_SHUTDOWN_WAIT_TIME_MSEC);
writeToLog("Regular shutdown time expired, shutting down hard using System.exit()");
System.exit(0);
} catch (InterruptedException e) {
writeToLog("Received expected interrupt before the shutdown timeout expired");
}
}
}
}
});
}
private Socket waitForConnection(ServerSocket serverSocket) throws IOException {
writeToLog("Waiting for connection at port " + serverSocket.getLocalPort());
Socket socket = serverSocket.accept(); // blocking wait
writeToLog("Accepted connection at port " + socket.getLocalPort());
return socket;
}
private String readMessage(Socket socket) throws IOException {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
char[] buffer = new char[BUFFERSIZE];
int length = bufferedReader.read(buffer, 0, BUFFERSIZE);
String message = new String(buffer, 0, length);
return message;
}
// SENDER PART
private void sendShutdownTokenInternal(File shutdownDataDir) throws IOException, UnknownHostException {
File secretFile = new File(shutdownDataDir, TOKEN_FILENAME);
String content;
try {
content = FileUtils.readFileToString(secretFile);
} catch (IOException e) {
logger.error("Failed to load shutdown configuration file: " + e.getMessage());
throw e;
}
// writeToLog("Loaded shutdown configuration " + content + " from file " + secretFile.getAbsolutePath());
int port = Integer.parseInt(StringUtils.splitAndUnescape(content)[0]);
String secret = StringUtils.splitAndUnescape(content)[1];
Socket socket = new Socket(HOST, port);
String message = "shutdown " + secret;
writeToLog("Sending \"" + message + "\" to " + HOST + ":" + port);
writeMessageToConnection(socket, message);
}
private void tryToRemoveInternalProfileDir() {
// this will only delete the directory if it is empty, so there is no harm in trying
if (!bootstrapSettings.getInternalDataDirectory().delete()) {
logger.warn("Failed to remove temporary profile directory " + bootstrapSettings.getInternalDataDirectory()
+ " although it should not contain any files");
}
}
private void writeMessageToConnection(Socket socket, String message) throws IOException {
PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()));
printWriter.print(message);
printWriter.flush();
}
}