blob: 4d96b54a2c8898887088bb9c8c533881b4a02491 [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.geode.admin.jmx.internal;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintStream;
import java.io.Serializable;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.SortedMap;
import java.util.StringTokenizer;
import java.util.TreeMap;
import org.apache.logging.log4j.Logger;
import org.apache.geode.GemFireException;
import org.apache.geode.SystemFailure;
import org.apache.geode.admin.AdminException;
import org.apache.geode.admin.jmx.Agent;
import org.apache.geode.admin.jmx.AgentConfig;
import org.apache.geode.admin.jmx.AgentFactory;
import org.apache.geode.distributed.internal.ClusterDistributionManager;
import org.apache.geode.internal.ExitCode;
import org.apache.geode.internal.OSProcess;
import org.apache.geode.internal.net.SocketCreator;
import org.apache.geode.internal.util.IOUtils;
import org.apache.geode.internal.util.JavaCommandBuilder;
import org.apache.geode.logging.internal.executors.LoggingThread;
import org.apache.geode.logging.internal.log4j.api.LogService;
/**
* A command line utility that is responsible for administering a stand-alone GemFire JMX
* {@link Agent}.
*
* @since GemFire 3.5
*/
public class AgentLauncher {
private static final Logger logger = LogService.getLogger();
/** Should the launch command be printed? */
public static final boolean PRINT_LAUNCH_COMMAND =
Boolean.getBoolean(AgentLauncher.class.getSimpleName() + ".PRINT_LAUNCH_COMMAND");
/* constants used to define state */
static final int SHUTDOWN = 0;
static final int STARTING = 1;
static final int RUNNING = 2;
static final int SHUTDOWN_PENDING = 3;
static final int SHUTDOWN_PENDING_AFTER_FAILED_STARTUP = 4;
static final int UNKNOWN = 6;
/** Agent configuration options */
static final String AGENT_PROPS = "agent-props";
/**
* A flag to indicate if the current log file should be kept. Used only when 'start' is used to
* fork off the 'server'
*/
static final String APPENDTO_LOG_FILE = "appendto-log-file";
/** optional and additional classpath entries */
static final String CLASSPATH = "classpath";
/** The directory argument */
static final String DIR = "dir";
/** Extra VM arguments */
static final String VMARGS = "vmargs";
/** The directory in which the agent's output resides */
private File workingDirectory = null;
/** The Status object for the agent */
private Status status = null;
/** base name for the agent to be launched */
private final String basename;
/** The name for the start up log file */
private final String startLogFileName;
/** The name of the status file */
private final String statusFileName;
/**
* Instantiates an AgentLauncher for execution and control of the GemFire JMX Agent process. This
* constructor is package private to prevent direct instantiation or subclassing by classes
* outside this package, but does allow the class to be tested as needed.
* <p/>
*
* @param basename base name for the application to be launched
*/
AgentLauncher(final String basename) {
assert basename != null : "The base name used by the AgentLauncher to create files cannot be null!";
this.basename = basename;
final String formattedBasename = this.basename.toLowerCase().replace(" ", "");
this.startLogFileName = "start_" + formattedBasename + ".log";
this.statusFileName = "." + formattedBasename + ".ser";
}
/**
* Prints information about the agent configuration options
*/
public void configHelp() {
PrintStream out = System.out;
Properties props = AgentConfigImpl.getDefaultValuesForAllProperties();
out.println("\n");
out.println("Agent configuration properties");
SortedMap<String, String> map = new TreeMap<String, String>();
int maxLength = 0;
for (Iterator<Object> iter = props.keySet().iterator(); iter.hasNext();) {
String prop = (String) iter.next();
int length = prop.length();
if (length > maxLength) {
maxLength = length;
}
map.put(prop,
AgentConfigImpl.getPropertyDescription(prop) + " (Default \""
+ props.getProperty(prop) + "\")");
}
Iterator<Entry<String, String>> entries = map.entrySet().iterator();
while (entries.hasNext()) {
Entry<String, String> entry = entries.next();
String prop = entry.getKey();
out.print(" ");
out.println(prop);
String description = entry.getValue();
StringTokenizer st = new StringTokenizer(description, " ");
out.print(" ");
int printed = 6;
while (st.hasMoreTokens()) {
String word = st.nextToken();
if (printed + word.length() > 72) {
out.print("\n ");
printed = 6;
}
out.print(word);
out.print(" ");
printed += word.length() + 1;
}
out.println("");
}
out.println("");
ExitCode.FATAL.doSystemExit();
}
/**
* Returns a map that maps the name of the start options to its value on the command line. If no
* value is specified on the command line, a default one is provided.
*/
protected Map<String, Object> getStartOptions(final String[] args) throws Exception {
final Map<String, Object> options = new HashMap<String, Object>();
options.put(APPENDTO_LOG_FILE, "false");
options.put(DIR, IOUtils.tryGetCanonicalFileElseGetAbsoluteFile(new File(".")));
final List<String> vmArgs = new ArrayList<String>();
options.put(VMARGS, vmArgs);
final Properties agentProps = new Properties();
options.put(AGENT_PROPS, agentProps);
for (final String arg : args) {
if (arg.startsWith("-classpath=")) {
options.put(CLASSPATH, arg.substring("-classpath=".length()));
} else if (arg.startsWith("-dir=")) {
final File workingDirectory = processDirOption(options, arg.substring("-dir=".length()));
System.setProperty(AgentConfigImpl.AGENT_PROPSFILE_PROPERTY_NAME,
new File(workingDirectory, AgentConfig.DEFAULT_PROPERTY_FILE).getPath());
} else if (arg.startsWith("-J")) {
vmArgs.add(arg.substring(2));
} else if (arg.contains("=")) {
final int index = arg.indexOf("=");
final String prop = arg.substring(0, index);
final String value = arg.substring(index + 1);
// if appendto-log-file is set, put it in options; it is not set as an agent prop
if (prop.equals(APPENDTO_LOG_FILE)) {
options.put(APPENDTO_LOG_FILE, value);
continue;
}
// verify the property is valid
AgentConfigImpl.getPropertyDescription(prop);
// Note, the gfAgentPropertyFile System property is ultimately read in the constructor of
// the AgentImpl class
// in order to make any properties defined in this file not only accessible to the
// DistributedSystem but to
// the GemFire Agent as well.
if (AgentConfigImpl.PROPERTY_FILE_NAME.equals(prop)) {
System.setProperty(AgentConfigImpl.AGENT_PROPSFILE_PROPERTY_NAME, value);
}
// The Agent properties file (specified with the command-line key=value) is used to pass
// configuration settings
// to the GemFire DistributedSystem. A property file can be passed using the property-file
// command-line switch
// is a large number of properties are specified, or the properties maybe individually
// specified on the
// command-line as property=value arguments.
agentProps.setProperty(prop, value);
}
}
return options;
}
/**
* After parsing the command line arguments, spawn the Java VM that will host the GemFire JMX
* Agent.
*/
public void start(final String[] args) throws Exception {
final Map<String, Object> options = getStartOptions(args);
workingDirectory = IOUtils.tryGetCanonicalFileElseGetAbsoluteFile((File) options.get(DIR));
// verify that any GemFire JMX Agent process has been properly shutdown and delete any remaining
// status files...
verifyAndClearStatus();
// start the GemFire JMX Agent process...
runCommandLine(options, buildCommandLine(options));
// wait for the GemFire JMX Agent process to complete startup and begin running...
// it is also possible the Agent process may fail to start, so this should not wait indefinitely
// unless
// the status file was not successfully written to
pollAgentUntilRunning();
ExitCode.NORMAL.doSystemExit();
}
private void verifyAndClearStatus() throws Exception {
final Status status = getStatus();
if (status != null && status.state != SHUTDOWN) {
throw new IllegalStateException(
"JMX Agent exists but was not shutdown.");
}
deleteStatus();
}
private String[] buildCommandLine(final Map<String, Object> options) {
final List<String> commands = JavaCommandBuilder.buildCommand(AgentLauncher.class.getName(),
(String) options.get(CLASSPATH), null, (List<String>) options.get(VMARGS));
commands.add("server");
commands.add("-dir=" + workingDirectory);
final Properties agentProps = (Properties) options.get(AGENT_PROPS);
for (final Object key : agentProps.keySet()) {
commands.add(key + "=" + agentProps.get(key.toString()));
}
return commands.toArray(new String[0]);
}
private void printCommandLine(final String[] commandLine) {
if (PRINT_LAUNCH_COMMAND) {
System.out.print("Starting " + this.basename + " with command:\n");
for (final String command : commandLine) {
System.out.print(command);
System.out.print(' ');
}
System.out.println();
}
}
private int runCommandLine(final Map<String, Object> options, final String[] commandLine)
throws IOException {
// initialize the startup log starting with a fresh log file (where all startup messages are
// printed)
final File startLogFile = IOUtils
.tryGetCanonicalFileElseGetAbsoluteFile(new File(workingDirectory, startLogFileName));
if (startLogFile.exists() && !startLogFile.delete()) {
throw new IOException(String.format("Unable to delete file %s.",
startLogFile.getAbsolutePath()));
}
Map<String, String> env = new HashMap<String, String>();
// read the passwords from command line
SocketCreator.readSSLProperties(env, true);
printCommandLine(commandLine);
final int pid = OSProcess.bgexec(commandLine, workingDirectory, startLogFile, false, env);
System.out.println(
String.format("Starting JMX Agent with pid: %s", pid));
return pid;
}
private void pollAgentUntilRunning() throws Exception {
Status status = spinReadStatus();
// TODO this loop could recurse indefinitely if the GemFire JMX Agent's state never changes from
// STARTING
// to something else (like RUNNING), which could happen if server process fails to startup
// correctly
// and did not or could not write to the status file!
// TODO should we really allow the InterruptedException from the Thread.sleep call to break this
// loop (yeah, I
// think so given the fact this could loop indefinitely)?
while (status != null && status.state == STARTING) {
Thread.sleep(500);
status = spinReadStatus();
}
if (status == null) {
// TODO throw a more appropriate Exception here!
throw new Exception("No available status.");
} else {
System.out.println(status);
}
}
/**
* Starts the GemFire JMX Agent "server" process with the given command line arguments.
*/
public void server(final String[] args) throws Exception {
final Map<String, Object> options = getStartOptions(args);
workingDirectory = IOUtils.tryGetCanonicalFileElseGetAbsoluteFile((File) options.get(DIR));
writeStatus(createStatus(this.basename, STARTING, OSProcess.getId()));
final Agent agent = createAgent((Properties) options.get(AGENT_PROPS));
final Thread thread = createAgentProcessThread(agent);
thread.start();
// periodically check and see if the JMX Agent has been told to stop
pollAgentForPendingShutdown(agent);
}
private Agent createAgent(final Properties props) throws IOException, AdminException {
ClusterDistributionManager.setIsDedicatedAdminVM(true);
SystemFailure.setExitOK(true);
final AgentConfigImpl config = new AgentConfigImpl(props);
// see bug 43760
if (config.getLogFile() == null || "".equals(config.getLogFile().trim())) {
config.setLogFile(AgentConfigImpl.DEFAULT_LOG_FILE);
}
// LOG:TODO: redirectOutput called here
OSProcess.redirectOutput(new File(config.getLogFile())); // redirect output to the configured
// log file
return AgentFactory.getAgent(config);
}
private UncaughtExceptionHandler createUncaughtExceptionHandler() {
return (t, e) -> {
if (e instanceof VirtualMachineError) {
SystemFailure.setFailure((VirtualMachineError) e);
}
setServerError(String.format("Uncaught exception in thread %s",
t.getName()), e);
};
}
private Thread createAgentProcessThread(final Agent agent) {
Thread thread = new LoggingThread("Start agent", createAgentProcessRunnable(agent));
thread.setUncaughtExceptionHandler(createUncaughtExceptionHandler());
return thread;
}
private Runnable createAgentProcessRunnable(final Agent agent) {
return new Runnable() {
@Override
public void run() {
try {
agent.start();
writeStatus(createStatus(AgentLauncher.this.basename, RUNNING, OSProcess.getId()));
} catch (IOException e) {
e.printStackTrace();
} catch (GemFireException e) {
e.printStackTrace();
handleGemFireException(e);
}
}
private void handleGemFireException(final GemFireException e) {
String message = String.format("Server failed to start: %s",
e.getMessage());
if (e.getCause() != null) {
if (e.getCause().getCause() != null) {
message += ", " + e.getCause().getCause().getMessage();
}
}
setServerError(null, new Exception(message));
}
};
}
/**
* Notes that an error has occurred in the agent and that it has shut down because of it.
*/
private void setServerError(final String message, final Throwable cause) {
try {
writeStatus(createStatus(this.basename, SHUTDOWN_PENDING_AFTER_FAILED_STARTUP,
OSProcess.getId(), message, cause));
} catch (Exception e) {
logger.fatal(e.getMessage(), e);
ExitCode.FATAL.doSystemExit();
}
}
private void pollAgentForPendingShutdown(final Agent agent) throws Exception {
while (true) {
pause(500);
spinReadStatus();
if (isStatus(SHUTDOWN_PENDING, SHUTDOWN_PENDING_AFTER_FAILED_STARTUP)) {
agent.stop();
final ExitCode exitCode =
(isStatus(SHUTDOWN_PENDING_AFTER_FAILED_STARTUP) ? ExitCode.FATAL : ExitCode.NORMAL);
writeStatus(createStatus(this.status, SHUTDOWN));
exitCode.doSystemExit();
}
}
}
/**
* Extracts configuration information for stopping a agent based on the contents of the command
* line. This method can also be used with getting the status of a agent.
*/
protected Map<String, Object> getStopOptions(final String[] args) throws Exception {
final Map<String, Object> options = new HashMap<String, Object>();
options.put(DIR, IOUtils.tryGetCanonicalFileElseGetAbsoluteFile(new File(".")));
for (final String arg : args) {
if (arg.equals("stop") || arg.equals("status")) {
// expected
} else if (arg.startsWith("-dir=")) {
processDirOption(options, arg.substring("-dir=".length()));
} else {
throw new Exception(
String.format("Unknown argument: %s", arg));
}
}
return options;
}
/**
* Stops a running JMX Agent by setting the status to "shutdown pending".
*/
public void stop(final String[] args) throws Exception {
final Map<String, Object> options = getStopOptions(args);
workingDirectory = IOUtils.tryGetCanonicalFileElseGetAbsoluteFile((File) options.get(DIR));
ExitCode exitCode = ExitCode.FATAL;
if (new File(workingDirectory, statusFileName).exists()) {
spinReadStatus();
if (!isStatus(SHUTDOWN)) {
writeStatus(createStatus(this.basename, SHUTDOWN_PENDING, status.pid));
}
pollAgentForShutdown();
if (isStatus(SHUTDOWN)) {
System.out
.println(String.format("The %s has shut down.", this.basename));
deleteStatus();
exitCode = ExitCode.NORMAL;
} else {
System.out
.println(String.format("Timeout waiting for %s to shutdown, status is: %s",
this.basename, status));
}
} else {
System.out.println(
String.format("The specified working directory (%s) contains no status file",
workingDirectory));
}
exitCode.doSystemExit();
}
private void pollAgentForShutdown() throws InterruptedException {
final long endTime = (System.currentTimeMillis() + 20000);
long clock = 0;
while (clock < endTime && !isStatus(SHUTDOWN)) {
pause(500);
spinReadStatus();
clock = System.currentTimeMillis();
}
}
/**
* Prints the status of the GemFire JMX Agent running in the configured working directory.
*/
public void status(final String[] args) throws Exception {
this.workingDirectory =
IOUtils.tryGetCanonicalFileElseGetAbsoluteFile((File) getStopOptions(args).get(DIR));
System.out.println(getStatus());
ExitCode.NORMAL.doSystemExit();
}
/**
* Returns the <code>Status</code> of the GemFire JMX Agent in the <code>workingDirectory</code>.
*/
protected Status getStatus() throws Exception {
Status status;
if (new File(workingDirectory, statusFileName).exists()) {
status = spinReadStatus();
} else {
status = createStatus(this.basename, SHUTDOWN, 0,
String.format("%s is not running in the specified working directory: (%s).",
this.basename, this.workingDirectory),
null);
}
return status;
}
/**
* Determines if the Status.state is one of the specified states in the given array of states.
* Note, the status of the Agent, as indicated in the .agent.ser status file, should never have a
* written value of UNKNOWN.
* <p/>
*
* @param states an array of possible acceptable states satisfying the condition of the Agent's
* status.
* @return a boolean value indicating whether the Agent's status satisfies one of the specified
* states.
*/
private boolean isStatus(final Integer... states) {
return (this.status != null
&& Arrays.asList(defaultToUnknownStateIfNull(states)).contains(this.status.state));
}
/**
* Removes an agent's status file
*/
protected void deleteStatus() throws IOException {
deleteStatus(workingDirectory);
}
void deleteStatus(final File workingDirectory) throws IOException {
final File statusFile = new File(workingDirectory, statusFileName);
if (statusFile.exists() && !statusFile.delete()) {
throw new IOException("Could not delete status file (" + statusFile.getAbsolutePath() + ")");
}
}
/**
* Reads the GemFire JMX Agent's status from the status file (.agent.ser) in it's working
* directory.
* <p/>
*
* @return a Status object containing the state persisted to the .agent.ser file in the working
* directory and representing the status of the Agent
* @throws IOException if the status file was unable to be read.
* @throws RuntimeException if the class of the object written to the .agent.ser file is not of
* type Status.
*/
protected Status readStatus() throws IOException {
return readStatus(workingDirectory);
}
Status readStatus(final File workingDirectory) throws IOException {
FileInputStream fileIn = null;
ObjectInputStream objectIn = null;
try {
fileIn = new FileInputStream(new File(workingDirectory, statusFileName));
objectIn = new ObjectInputStream(fileIn);
this.status = (Status) objectIn.readObject();
return this.status;
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} finally {
IOUtils.close(objectIn);
IOUtils.close(fileIn);
}
}
/**
* Reads the JMX Agent's status from the .agent.ser status file. If the status file cannot be read
* due to I/O problems, the method will keep attempting to read the file for up to 20 seconds.
* <p/>
*
* @return the Status of the GemFire JMX Agent as determined by the .agent.ser status file, or
* natively based on the presence/absence of the Agent process.
*/
protected Status spinReadStatus() {
Status status = null;
final long endTime = (System.currentTimeMillis() + 20000);
long clock = 0;
while (status == null && clock < endTime) {
try {
status = readStatus();
} catch (Exception ignore) {
// see bug 31575
// see bug 36998
// try again after a short delay... the status file might have been read prematurely before
// it existed
// or while the server was trying to write to it resulting in a possible EOFException, or
// other IOException.
pause(500);
} finally {
clock = System.currentTimeMillis();
}
}
return status;
}
/**
* Sets the status of the GemFire JMX Agent by serializing a <code>Status</code> object to a
* status file in the Agent's working directory.
* <p/>
*
* @param status the Status object representing the state of the Agent process to persist to disk.
* @return the written Status object.
* @throws IOException if the Status could not be successfully persisted to disk.
*/
public Status writeStatus(final Status status) throws IOException {
return writeStatus(status, workingDirectory);
}
Status writeStatus(final Status status, final File workingDirectory) throws IOException {
FileOutputStream fileOut = null;
ObjectOutputStream objectOut = null;
try {
fileOut = new FileOutputStream(new File(workingDirectory, statusFileName));
objectOut = new ObjectOutputStream(fileOut);
objectOut.writeObject(status);
objectOut.flush();
this.status = status;
return this.status;
} finally {
IOUtils.close(objectOut);
IOUtils.close(fileOut);
}
}
protected static Status createStatus(final String basename, final int state, final int pid) {
return createStatus(basename, state, pid, null, null);
}
protected static Status createStatus(final String basename, final int state, final int pid,
final String msg, final Throwable t) {
final Status status = new Status(basename);
status.state = state;
status.pid = pid;
status.msg = msg;
status.exception = t;
return status;
}
protected static Status createStatus(final Status status, final int state) {
assert status != null : "The status to clone cannot be null!";
return createStatus(status.baseName, state, status.pid, status.msg, status.exception);
}
protected static Integer[] defaultToUnknownStateIfNull(final Integer... states) {
return (states != null ? states : new Integer[] {UNKNOWN});
}
protected static boolean pause(final int milliseconds) {
try {
Thread.sleep(milliseconds);
return true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
protected static File processDirOption(final Map<String, Object> options, final String dirValue)
throws FileNotFoundException {
final File workingDirectory = new File(dirValue);
if (!workingDirectory.exists()) {
throw new FileNotFoundException(
String.format("The input working directory does not exist: %s",
dirValue));
}
options.put(DIR, workingDirectory);
return workingDirectory;
}
/**
* Prints usage information for the AgentLauncher to the command line.
* <p/>
*
* @param message a String to output to the command line indicating the user error.
*/
private static void usage(final String message) {
final PrintStream out = System.out;
out.println("\n** " + message + "\n");
out.println("agent start [-J<vmarg>]* [-dir=<dir>] [prop=value]*");
out.println("Starts the GemFire JMX Agent");
out.println("\t"
+ "<vmarg> a VM-option passed to the agent's VM, example -J-Xmx1024M for a 1 Gb heap");
out.println("\t<dir> Directory in which agent runs, default is the current directory");
out.println("\t<prop> A configuration property/value passed to the agent");
out.println("\t(see help config for more details)");
out.println();
out.println("agent stop [-dir=<dir>]");
out.println("Stops a GemFire JMX Agent");
out.println("\t<dir> Directory in which agent runs, default is the current directory");
out.println("");
out.println("agent status [-dir=<dir>]");
out.println(
"Reports the status and the process id of a GemFire JMX Agent");
out.println("\t<dir> Directory in which agent runs, default is the current directory");
out.println();
ExitCode.FATAL.doSystemExit();
}
/**
* Bootstrap method to launch the GemFire JMX Agent process to monitor and manage a GemFire
* Distributed System/Cache. Main will read the arguments passed on the command line and dispatch
* the command to the appropriate handler.
*/
public static void main(final String[] args) {
if (args.length < 1) {
usage("Missing command");
}
// TODO is this only needed on 'agent server'? 'agent {start|stop|status}' technically do no run
// any GemFire Cache
// or DS code inside the current process.
SystemFailure.loadEmergencyClasses();
final AgentLauncher launcher = new AgentLauncher("Agent");
try {
final String command = args[0];
if (command.equalsIgnoreCase("start")) {
launcher.start(args);
} else if (command.equalsIgnoreCase("server")) {
launcher.server(args);
} else if (command.equalsIgnoreCase("stop")) {
launcher.stop(args);
} else if (command.equalsIgnoreCase("status")) {
launcher.status(args);
} else if (command.toLowerCase().matches("-{0,2}help")) {
if (args.length > 1) {
final String topic = args[1];
if (topic.equals("config")) {
launcher.configHelp();
} else {
usage(String.format("No help available for %s", topic));
}
}
usage("agent help");
} else {
usage(String.format("Unknown command: %s", command));
}
} catch (VirtualMachineError e) {
SystemFailure.initiateFailure(e);
throw e;
} catch (Throwable t) {
SystemFailure.checkFailure();
t.printStackTrace();
System.err.println(
String.format("Error : %s", t.getLocalizedMessage()));
ExitCode.FATAL.doSystemExit();
}
}
/**
* A class representing the current state of the GemFire JMX Agent process. Instances of this
* class are serialized to a {@linkplain #statusFileName file} on disk in the specified working
* directory {@linkplain #workingDirectory}.
* <p/>
*
* @see #SHUTDOWN
* @see #STARTING
* @see #RUNNING
* @see #SHUTDOWN_PENDING
* @see #SHUTDOWN_PENDING_AFTER_FAILED_STARTUP
*/
// TODO refactor this class and internalize the state
// TODO refactor the class and make immutable
static class Status implements Serializable {
private static final long serialVersionUID = -7758402454664266174L;
int pid = 0;
int state = 0;
final String baseName;
String msg;
Throwable exception;
public Status(final String baseName) {
this.baseName = baseName;
}
@Override
public String toString() {
final StringBuilder buffer = new StringBuilder();
if (pid == Integer.MIN_VALUE && state == SHUTDOWN && msg != null) {
buffer.append(msg);
} else {
buffer.append(
String.format("%s pid: %d status: ", this.baseName, pid));
switch (state) {
case SHUTDOWN:
buffer.append("shutdown");
break;
case STARTING:
buffer.append("starting");
break;
case RUNNING:
buffer.append("running");
break;
case SHUTDOWN_PENDING:
buffer.append("shutdown pending");
break;
case SHUTDOWN_PENDING_AFTER_FAILED_STARTUP:
buffer.append("shutdown pending after failed startup");
break;
default:
buffer.append("unknown");
break;
}
if (exception != null) {
if (msg != null) {
buffer.append("\n").append(msg).append(" - ");
} else {
buffer.append("\n " + String.format("Exception in %s : %s ",
this.baseName, exception.getMessage()) + " - ");
}
buffer
.append("See log file for details.");
}
}
return buffer.toString();
}
}
}