package com.subhajit.managedprocess;
import static com.subhajit.processmanager.api.IHeartbeat.INTERVAL;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanException;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.ReflectionException;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import org.apache.tools.ant.taskdefs.ManifestException;
import com.subhajit.build.Manifest;
import com.subhajit.codeanalysis.distribution.DistributionManager;
import com.subhajit.common.util.StrUtils;
import com.subhajit.common.util.process.ProcessRunner;
import com.subhajit.common.util.streams.NetUtils;
import com.subhajit.processmanager.agent.ProcessManagerMain;
import com.subhajit.processmanager.agent.heartbeat.Heartbeat;
import com.subhajit.processmanager.agent.heartbeat.HeartbeatRepositoryImpl;
import com.subhajit.processmanager.api.IHeartbeat;
import com.subhajit.processmanager.api.IHeartbeatRepository;
/**
* An operating-system process that can be remotely managed.
*
* @author sdasgupta
*
*/
public class ManagedProcess {
private static final long TWICE_THE_INTERVAL = 10 * INTERVAL;
private final String uid;
private final IHeartbeatRepository repo;
private ManagedProcessStatus status;
private final ProcessBuilder processBuilder;
private final String description;
public ManagedProcess(String description, ProcessBuilder processBuilder)
throws IOException {
super();
this.description = description;
uid = UUID.randomUUID().toString();
repo = new HeartbeatRepositoryImpl(uid);
status = ManagedProcessStatus.UNKNOWN;
this.processBuilder = copy(processBuilder);
ManagedProcesses.getDefaultInstance().add(this);
}
private static ProcessBuilder copy(ProcessBuilder processBuilder) {
ProcessBuilder ret = new ProcessBuilder();
ret.command(processBuilder.command());
ret.directory(processBuilder.directory());
ret.environment().putAll(processBuilder.environment());
return ret;
}
public static final class StartupResult implements Serializable {
private static final long serialVersionUID = 1L;
private final String pid;
private final OutputStream standardOutput;
private final OutputStream standardError;
public StartupResult(String pid, OutputStream standardOutput,
OutputStream standardError) {
super();
this.pid = pid;
this.standardOutput = standardOutput;
this.standardError = standardError;
}
public String getPid() {
return pid;
}
public OutputStream getStandardOutput() {
return standardOutput;
}
public OutputStream getStandardError() {
return standardError;
}
}
/**
* Convenience method invokes
* {@link ManagedProcess#startup(OutputStream, OutputStream)} and returns a
* {@link StartupResult}.
*
* @return
* @throws IOException
* @throws InterruptedException
* @throws ClassNotFoundException
* @throws ManifestException
* @throws ExecutionException
*/
public synchronized StartupResult startup() throws IOException,
InterruptedException, ClassNotFoundException, ManifestException,
ExecutionException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayOutputStream err = new ByteArrayOutputStream();
return new StartupResult(startup(out, err), out, err);
}
/**
* Launches the process described by {@link ManagedProcess#processBuilder}
* and returns a process id that can be used to identify the process.
*
* <p>
* <tt>builder</tt> must satisfy the following requirements:
* <li><tt>builder.command()</tt> must not be <tt>null</tt> and
* <tt>builder.command().size() > 2</tt></li>
* <li>builder.directory() must not be <tt>null</tt> and must exist.</li>
* </p>
*
* @param builder
* @param out
* @param err
* @return
* @throws IOException
* @throws InterruptedException
* @throws ClassNotFoundException
* @throws ManifestException
* @throws ExecutionException
*/
public synchronized String startup(OutputStream out, OutputStream err)
throws IOException, InterruptedException, ClassNotFoundException,
ManifestException, ExecutionException {
ProcessBuilder builder = copy(processBuilder);
// Check valid state before attempting to launch the process.
boolean valid = status == ManagedProcessStatus.UNKNOWN
|| status == ManagedProcessStatus.STOPPED;
if (!valid) {
throw new IllegalStateException(
"Cannot launch, since status is neither "
+ ManagedProcessStatus.STOPPED + " nor "
+ ManagedProcessStatus.UNKNOWN);
}
final List<String> command = new ArrayList<String>(builder.command());
if (command.size() < 2) {
throw new IllegalArgumentException("Bad builder command : size < 2");
}
// Add the remote debugging arguments.
int remoteDebuggerPort = NetUtils.findFreePort(10000) + 1;
String debuggingOption = StrUtils
.strReplace(
"-Xdebug -Xrunjdwp:transport=dt_socket,address=${port},server=y,suspend=n",
"${port}", "" + remoteDebuggerPort);
command.add(1, "-Xdebug " + debuggingOption + " ");
if (ProcessRunner.isWindows()) {
command.add(0, "start \"" + description + "\" /LOW /MIN ");
}
final File dir = builder.directory();
if (dir == null) {
throw new IllegalArgumentException("Run directory not given.");
}
// Export the process-manager.jar file to the "dir" directory.
Class<?> klass = ProcessManagerMain.class;
Manifest manifest = new Manifest();
manifest.getMainSection().addConfiguredAttribute(
new Manifest.Attribute("Premain-Class",
ProcessManagerMain.class.getName()));
DistributionManager.createDistributionWithAdditionalClasses(Thread
.currentThread().getContextClassLoader(), new File(dir,
"process-manager.jar"), manifest, klass.getName(),
com.subhajit.processmanager.agent.Process.class.getName(),
HeartbeatRepositoryImpl.class.getName(), Heartbeat.class
.getName());
command.add(2, "-javaagent:process-manager.jar");
command.add(2, "-Duid=" + uid);
if ( ProcessRunner.isLinux() || ProcessRunner.isSolaris() ){
command.add("&");
}
builder.command(command);
// Launch the process.
ProcessRunner.execViaShell(builder, out, err, true);
status = ManagedProcessStatus.STARTING;
return uid;
}
/**
* Returns the last heart beat written by the managed process.
*
* @param millis
* @return
* @throws InterruptedException
*/
public IHeartbeat getHeartbeat(final long millis)
throws InterruptedException {
final List<IHeartbeat> holder = new ArrayList<IHeartbeat>();
Thread thread = new Thread(new Runnable() {
public void run() {
try {
IHeartbeat ret = repo.getLatest(uid);
if (ret == null) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
ret = repo.getLatest(uid);
if (ret != null) {
holder.add(ret);
}
return;
} else {
holder.add(ret);
return;
}
} catch (IOException exc) {
}
}
});
thread.start();
thread.join();
if (holder.isEmpty()) {
return null;
} else {
return holder.get(0);
}
}
public String getUid() {
return uid;
}
/**
* Issues a <tt>shutdown</tt> command to the managed process and returns
* immediately.
*
* @param millis
* @return {@link ManagedProcessStatus#UNKNOWN} if the heart beat for the
* process is not obtained within <tt>millis</tt> milli-seconds, or
* {@link ManagedProcessStatus#STOPPING} if a a <tt>stop</tt>
* command was issued to the server.
* @throws InterruptedException
* @throws IOException
* @throws InstanceNotFoundException
* @throws MalformedObjectNameException
* @throws MBeanException
* @throws ReflectionException
* @throws NullPointerException
*/
public ManagedProcessStatus shutdown(long millis)
throws InterruptedException, IOException,
InstanceNotFoundException, MalformedObjectNameException,
MBeanException, ReflectionException, NullPointerException {
if (status != ManagedProcessStatus.RUNNING) {
throw new IllegalStateException("Cannot stop process in state - "
+ status.toString());
}
IHeartbeat heartbeat = getHeartbeat(millis);
if (heartbeat == null) {
return ManagedProcessStatus.UNKNOWN;
}
JMXServiceURL url = new JMXServiceURL(heartbeat.getManagementUrl());
JMXConnector connector = null;
try {
connector = JMXConnectorFactory.connect(url);
connector.getMBeanServerConnection().invoke(
new ObjectName("launcher", "id", "bean"), "shutdown",
new Object[0], new String[] {});
return ManagedProcessStatus.STOPPING;
} catch (IOException exc) {
return ManagedProcessStatus.STOPPING;
} finally {
if (connector != null) {
try {
connector.close();
} catch (IOException exc) {
exc.printStackTrace();
}
}
}
}
@Override
public boolean equals(Object o) {
if (o == null) {
return false;
}
if (this == o) {
return true;
}
if (o instanceof ManagedProcess) {
ManagedProcess other = (ManagedProcess) o;
return uid.equals(other.uid);
} else {
return false;
}
}
@Override
public int hashCode() {
return uid.hashCode();
}
public ManagedProcessStatus getStatus() throws InterruptedException {
ManagedProcessStatus status = updateStatus();
synchronized (this) {
this.status = status;
}
return status;
}
/**
* Updates {@link #status}.
*
* @return
* @throws InterruptedException
*/
private ManagedProcessStatus updateStatus() throws InterruptedException {
switch (status) {
case UNKNOWN:
return handleUnknownStatus();
case RUNNING:
return handleRunningStatus();
case START_FAILED:
return handleStartFailedStatus();
case STARTING:
return handleStartingStatus();
case STOPPED:
return handleStoppedStatus();
case STOPPING:
return handleStoppingStatus();
default: // This is most likely a programming error.
throw new AssertionError("Unknown status - " + status);
}
}
private ManagedProcessStatus handleStoppingStatus()
throws InterruptedException {
IHeartbeat heartbeat = getHeartbeat(INTERVAL);
if (heartbeat == null) {
heartbeat = getHeartbeat(TWICE_THE_INTERVAL);
if (heartbeat == null) {
return ManagedProcessStatus.STOPPED;
} else {
// Still stopping.
return ManagedProcessStatus.STOPPING;
}
} else {
if (isCurrent(heartbeat)) {
return ManagedProcessStatus.STOPPING;
} else {
heartbeat = getHeartbeat(TWICE_THE_INTERVAL);
if (heartbeat == null) {
return ManagedProcessStatus.STOPPED;
} else if (isCurrent(heartbeat, TWICE_THE_INTERVAL)) {
return ManagedProcessStatus.STOPPING;
} else {
return ManagedProcessStatus.STOPPED;
}
}
}
}
private ManagedProcessStatus handleStoppedStatus() {
// Nothing to do.
return ManagedProcessStatus.STOPPED;
}
/**
* Called after {@link #startup(ProcessBuilder, OutputStream, OutputStream)}
* has been invoked.
*
* @return
* @throws InterruptedException
*/
private ManagedProcessStatus handleStartingStatus()
throws InterruptedException {
IHeartbeat heartbeat = getHeartbeat(INTERVAL);
if (heartbeat == null) {
heartbeat = getHeartbeat(TWICE_THE_INTERVAL);
if (heartbeat == null) {
return ManagedProcessStatus.START_FAILED;
} else {
return ManagedProcessStatus.RUNNING;
}
} else {
if (isCurrent(heartbeat)) {
return ManagedProcessStatus.RUNNING;
} else {
heartbeat = getHeartbeat(TWICE_THE_INTERVAL);
if (heartbeat == null) {
return ManagedProcessStatus.START_FAILED;
} else {
return ManagedProcessStatus.RUNNING;
}
}
}
}
private boolean isCurrent(IHeartbeat heartbeat, long... intervals) {
long testInterval = intervals.length > 0 ? intervals[0] : INTERVAL;
return (System.currentTimeMillis() - heartbeat.getTime()) < testInterval;
}
private ManagedProcessStatus handleStartFailedStatus()
throws InterruptedException {
// Nothing to do.
return handleStartingStatus();
}
private ManagedProcessStatus handleRunningStatus()
throws InterruptedException {
IHeartbeat heartbeat = getHeartbeat(INTERVAL);
if (heartbeat == null) {
heartbeat = getHeartbeat(TWICE_THE_INTERVAL);
if (heartbeat == null) {
return ManagedProcessStatus.STOPPED;
} else {
// The process missed a beat.
return ManagedProcessStatus.RUNNING;
}
} else {
if (isCurrent(heartbeat)) {
return ManagedProcessStatus.RUNNING;
} else {
heartbeat = getHeartbeat(TWICE_THE_INTERVAL);
if (heartbeat == null) {
return ManagedProcessStatus.STOPPED;
} else if (!isCurrent(heartbeat, TWICE_THE_INTERVAL)) {
// The process crashed.
return ManagedProcessStatus.STOPPED;
} else {
return ManagedProcessStatus.RUNNING;
}
}
}
}
/**
* Happens when {@link #startup(ProcessBuilder, OutputStream, OutputStream)}
* has not been invoked for the first time, or the first heartbeat has not
* been detected.
*
* @return
* @throws InterruptedException
*/
private ManagedProcessStatus handleUnknownStatus()
throws InterruptedException {
// Nothing to do.
// return ManagedProcessStatus.UNKNOWN;
IHeartbeat heartbeat = getHeartbeat(INTERVAL);
if (heartbeat == null) {
heartbeat = getHeartbeat(TWICE_THE_INTERVAL);
if (heartbeat == null) {
return ManagedProcessStatus.UNKNOWN;
} else {
return ManagedProcessStatus.RUNNING;
}
} else {
if (isCurrent(heartbeat)) {
return ManagedProcessStatus.RUNNING;
} else {
heartbeat = getHeartbeat(TWICE_THE_INTERVAL);
if (heartbeat == null) {
return ManagedProcessStatus.UNKNOWN;
} else {
return ManagedProcessStatus.RUNNING;
}
}
}
}
public ManagedProcessStatus forceStop() {
if (status == ManagedProcessStatus.START_FAILED) {
status = ManagedProcessStatus.STOPPED;
return status;
} else {
throw new IllegalStateException(
"Force stop command not valid in this state - " + status);
}
}
@Override
public String toString() {
return getDescription() + " - " + uid + "\t{"
+ processBuilder.toString() + "}";
}
public String getDescription() {
return description;
}
}