/*
* Part of the CCNx Java Library.
*
* Copyright (C) 2008, 2009, 2010, 2011 Palo Alto Research Center, Inc.
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License version 2.1
* as published by the Free Software Foundation.
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details. You should have received
* a copy of the GNU Lesser General Public License along with this library;
* if not, write to the Free Software Foundation, Inc., 51 Franklin Street,
* Fifth Floor, Boston, MA 02110-1301 USA.
*/
package org.ccnx.ccn.impl.support;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.rmi.NoSuchObjectException;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.server.RemoteObject;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.Date;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import org.ccnx.ccn.config.SystemConfiguration;
import org.ccnx.ccn.config.SystemConfiguration.DEBUGGING_FLAGS;
import org.ccnx.ccn.impl.CCNNetworkManager;
/**
* Implements command line daemon functionality. In the normal case a daemon is started up and then runs in
* the background after the starting process exits. An RMI file is created to allow outside processes to
* communicate with the daemon to "signal" it or stop it.<p>
*
* Daemons have several modes as seen below:
*
* <pre>
* MODE_START - used to start a daemon which will be run in the background
* MODE_STOP - used to stop a daemon currently running in the background
* MODE_INTERACTIVE - used to run a daemon interactively rather than in the background
* MODE_DAEMON - a daemon started up by a "starter" is started in MODE_DAEMON.
* MODE_SIGNAL - used to signal a daemon from outside to get the daemon to perform implementation defined
* services
* </pre>
*/
public class Daemon {
protected enum Mode {MODE_UNKNOWN, MODE_START, MODE_STOP, MODE_INTERACTIVE, MODE_DAEMON, MODE_SIGNAL};
protected static Daemon _daemon;
protected String _daemonName = null;
protected static DaemonListenerClass _daemonListener = null;
protected boolean _interactive = false;
protected String _pid;
// TODO maybe this doesn't belong here
public static final String PROP_JAVA_LIBRARY_PATH ="java.library.path";
public static final String PROP_DAEMON_MEMORY = "ccn.daemon.memory";
public static final String PROP_DAEMON_DEBUG_PORT = "ccn.daemon.debug";
public static final String PROP_DAEMON_OUTPUT = "ccn.daemon.output";
public static final String PROP_DAEMON_PROFILE = "ccn.daemon.profile";
public static final String PROP_DAEMON_DEBUG_SUSPEND = "ccn.daemon.debug.suspend";
public static final String PROP_DAEMON_DEBUG_NOSHARE = "ccn.daemon.debug.noshare";
public static final String PROP_DAEMON_CHECK_JNI = "ccn.daemon.check.jni";
public static final String DEFAULT_OUTPUT_STREAM = "/dev/null";
/**
* Interface describing the RMI server object sitting inside
* the daemon
*/
public interface DaemonListener extends Remote {
public String startLoop() throws RemoteException; // returns pid
public void shutDown() throws RemoteException;
public boolean signal(String name) throws RemoteException;
public Object status(String name) throws RemoteException;
}
public class StopTimer implements Runnable {
private String _daemonName;
private String _pid;
private StopTimer(String daemonName, String pid) {
_daemonName = daemonName;
_pid = pid;
}
@Override
public void run() {
System.out.println("Attempt to contact daemon " + _daemonName + " timed out");
Log.info("Attempt to contact daemon " + _daemonName + " timed out");
try {
cleanupDaemon(_daemonName, _pid);
} catch (IOException e) {
Log.logStackTrace(Level.WARNING, e);
}
System.exit(1);
}
}
/**
* Stop ccnd on exit from daemon
*/
protected class ShutdownHook extends Thread {
public void run() {
try {
_daemon.rmRMIFile(SystemConfiguration.getPID());
} catch (IOException e) {}
}
}
/**
* The thread that runs inside the daemon, doing work
*/
protected static class WorkerThread extends Thread implements Serializable {
private static final long serialVersionUID = -4969812722104756329L;
boolean _keepGoing;
String _daemonName;
protected WorkerThread(String daemonName) {
_daemonName = daemonName;
}
public void run() {
_keepGoing = true;
System.out.println("Initializing daemon thread " + new Date().toString() +".");
Log.info("Initializing daemon thread " + new Date().toString() +".");
initialize();
System.out.println("Daemon thread started " + new Date().toString() +".");
Log.info("Daemon thread started " + new Date().toString() +".");
do {
try {
work();
} catch (Exception e) {
Log.warning("Error in daemon thread: " + e.getMessage());
Log.warningStackTrace(e);
}
if (_keepGoing) {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {}
}
} while (_keepGoing);
// ok, we were asked to shut down
System.out.println("Shutting down the daemon.");
Log.info("Shutting down the daemon.");
try {
UnicastRemoteObject.unexportObject(_daemonListener, true);
} catch (NoSuchObjectException e) {
}
getRMIFile(_daemonName, SystemConfiguration.getPID()).delete();
}
public void shutDown() {
_keepGoing = false;
finish();
interrupt();
}
/**
* Specialized by subclasses, called by worker thread.
*
*/
public void work() {
Log.info("Should not be here, in WorkerThread.work().");
}
public void initialize() {
Log.info("Should not be here, in WorkerThread.initialize().");
}
public void finish() {
Log.info("Should not be here, in WorkerThread.finish().");
}
public void waitForStart() {
Log.info("Should not be here, in waitForStart().");
}
public boolean signal(String name) {
Log.info("Should not be here, in WorkerThread.signal().");
return false;
}
public Object status(String name) {
return null; // We don't require implementers to implement this
}
}
protected static class DaemonListenerClass extends UnicastRemoteObject implements DaemonListener {
private static final long serialVersionUID = -9217344397211709762L;
protected WorkerThread _daemonThread;
public DaemonListenerClass(WorkerThread daemonThread) throws RemoteException {
_daemonThread = daemonThread;
}
public void shutDown() throws RemoteException {
_daemonThread.shutDown();
}
public String startLoop() throws RemoteException {
System.out.println("Starting the daemon loop.");
Log.info("Starting the daemon loop.");
try {
_daemonThread.start();
String pid = SystemConfiguration.getPID();
if (null != SystemConfiguration.getPID()) {
// PID is available on this platform and we have the startLoop()
// invocation so we can rename our RMI file and send PID back to controller.
renameRMIFile(_daemonThread._daemonName, pid);
}
return pid;
} catch(Exception e) {
throw new RemoteException(e.getMessage(), e);
}
}
public boolean signal(String name) throws RemoteException {
Log.info("Signal " + name);
try {
return _daemonThread.signal(name);
} catch (Exception e) {
throw new RemoteException(e.getMessage(), e);
}
}
public Object status(String name) throws RemoteException {
Log.info("Status " + name);
try {
return _daemonThread.status(name);
} catch (Exception e) {
throw new RemoteException(e.getMessage(), e);
}
}
}
public Daemon() {_daemonName = "namelessDaemon";}
public String daemonName() { return _daemonName; }
public void setPid(String pid) {
_pid = pid;
}
public String getPid() {
return _pid;
}
/**
* Overridden by subclasses.
*
*/
protected void usage() {
try {
System.out.println("usage: " + this.getClass().getName() + " -start | -stop <pid> | -interactive | -signal <name> <pid>");
} catch (Exception e) {
e.printStackTrace();
}
System.exit(0);
}
/**
* Overridden by subclasses
*/
protected void initialize(String[] args, Daemon daemon) {
System.out.println("Unknown option " + args[1]);
daemon.usage();
}
/**
* overridden by subclasses to make right type of thread.
* @return
*/
protected WorkerThread createWorkerThread() {
return new WorkerThread(daemonName());
}
protected static void setupRemoteAccess(Daemon daemon, WorkerThread wt) throws IOException {
if (wt == null)
wt = daemon.createWorkerThread();
_daemonListener = new DaemonListenerClass(wt);
Remote stub = RemoteObject.toStub(_daemonListener);
File tempFile = getRMITempFile(daemon.daemonName());
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(tempFile));
try {
out.writeObject(stub);
out.flush();
} finally {
out.close();
}
// always use name without PID at first, will rename
// when startloop() message comes along. This allows
// the controlling starter process to communicate
// without needing the pid of the child, though it
// does limit the spawn rate
// this is atomic
tempFile.renameTo(getRMIFile(daemon.daemonName(), null));
Runtime.getRuntime().addShutdownHook(daemon.new ShutdownHook());
}
private static void startDaemon(String daemonName, String daemonClass, String args[]) throws IOException, ClassNotFoundException {
String mypid = SystemConfiguration.getPID();
if (null == mypid) {
// PID unavailable on this platform so we are restricted
// to a single instance per daemon name
if (getRMIFile(daemonName, null).exists()) {
System.out.println("Daemon already running. Use '" + daemonName + " -stop' to kill the daemon.");
return;
}
} // else it doesn't matter if one is running we may start another
ArrayList<String> argList = new ArrayList<String>();
argList.add("java");
String classPath = System.getProperty("java.class.path");
String [] directories = null;
String sep = null;
if (classPath.contains(":")) {
sep = ":";
directories = classPath.split(":");
} else if (classPath.contains(";")) {
sep = ";"; // do we need to escape?
directories = classPath.split(";");
}
StringBuffer cp = new StringBuffer(
((null != directories) &&
(directories.length > 0)) ?
directories[0] : "");
if (null != directories) {
for (int i=1; i < directories.length; ++i) {
cp.append(sep);
cp.append(directories[i]);
}
}
/**
* Add properties
* TODO - we might want to add them all but for now these are
* the critical ones
*/
String libPath = System.getProperty(PROP_JAVA_LIBRARY_PATH);
if (libPath != null) {
argList.add("-D" + PROP_JAVA_LIBRARY_PATH + "=" + libPath);
}
String portval = System.getProperty(CCNNetworkManager.PROP_AGENT_PORT);
if (portval != null) {
argList.add("-D" + CCNNetworkManager.PROP_AGENT_PORT + "=" + portval);
}
String memval = System.getProperty(PROP_DAEMON_MEMORY);
if (memval != null)
argList.add("-Xmx" + memval);
String debugFlagVal = System.getProperty(SystemConfiguration.DEBUG_FLAG_PROPERTY);
if (debugFlagVal != null)
argList.add("-D" + SystemConfiguration.DEBUG_FLAG_PROPERTY + "=" + debugFlagVal);
String debugDirVal = System.getProperty(SystemConfiguration.DEBUG_DATA_DIRECTORY_PROPERTY);
if (debugDirVal != null)
argList.add("-D" + SystemConfiguration.DEBUG_DATA_DIRECTORY_PROPERTY + "=" + debugDirVal);
String suspend = System.getProperty(PROP_DAEMON_DEBUG_SUSPEND);
String doSuspend = suspend == null ? "n" : "y";
String debugPort = System.getProperty(PROP_DAEMON_DEBUG_PORT);
if (debugPort != null) {
argList.add("-Xrunjdwp:transport=dt_socket,address=" + debugPort + ",server=y,suspend=" + doSuspend);
} else if (doSuspend.equals("y"))
Log.info("Suspend requested without debug attach");
String unshared = System.getProperty(PROP_DAEMON_DEBUG_NOSHARE);
if (null != unshared)
argList.add("-Xshare:off");
String checkJNI = System.getProperty(PROP_DAEMON_CHECK_JNI);
if (null != checkJNI)
argList.add("-Xcheck:jni");
String profileInfo = System.getProperty(PROP_DAEMON_PROFILE);
if (profileInfo != null) {
argList.add(profileInfo);
}
argList.add("-cp");
argList.add(cp.toString());
argList.add(daemonClass);
argList.add("-daemon");
for (int i=1; i < args.length; ++i) {
argList.add(args[i]);
}
String cmd = "";
for (String arg : argList) {
cmd += arg + " ";
}
if (SystemConfiguration.checkDebugFlag(DEBUGGING_FLAGS.DUMP_DAEMONCMD)) {
FileOutputStream fos = new FileOutputStream("daemon_cmd.txt");
try {
fos.write(cmd.getBytes());
fos.flush();
} finally {
fos.close();
}
}
// Now actually start the daemon
String outputFile = System.getProperty(PROP_DAEMON_OUTPUT);
boolean doAppend = true;
DaemonOutput output = null;
Log.info("Starting daemon with command line: " + cmd);
ProcessBuilder pb = new ProcessBuilder(argList);
pb.redirectErrorStream(true);
if (null != outputFile) {
doAppend = false;
}
Process child = pb.start();
if (null != outputFile) {
FileOutputStream fos = new FileOutputStream(outputFile, doAppend);
output = new DaemonOutput(child.getInputStream(), fos);
}
// Initial RMI file never named with PID to permit
// us to read it without knowing PID. After
// daemon operation is started below the file
// will be renamed with PID if possible
// TODO - does this loop really make sense? Shouldn't the name change happen quickly or not at all?
while (!getRMIFile(daemonName, null).exists()) {
InputStream childMsgs = null;
try {
Thread.sleep(1000);
try {
if (null == outputFile) {
childMsgs = child.getInputStream();
} else
childMsgs = new FileInputStream(outputFile);
// The following should throw an IllegalThreadStateException in child.exitValue()
// If it doesn't then the startup failed
int exitValue = child.exitValue();
// if we get here, the child has exited
// Read and output to the console any messages from the child to help diagnose the problem
Log.warning("Could not launch daemon " + daemonName + ". Daemon exit value is " + exitValue + ".");
System.err.println("Could not launch daemon " + daemonName + ". Daemon exit value is " + exitValue + ".");
byte[] childMsgBytes = new byte[childMsgs.available()];
childMsgs.read(childMsgBytes);;
String childOutput = new String(childMsgBytes);
System.err.println("Messages from the child were: \"" + childOutput + "\"");
return;
} catch (IllegalThreadStateException e) {} // The daemon successfully started
finally {
if (null != childMsgs)
childMsgs.close();
}
} catch (InterruptedException e) {}
}
// The daemon has started running - now start its operation
ObjectInputStream in = new ObjectInputStream(new FileInputStream(getRMIFile(daemonName, null)));
DaemonListener l;
try {
l = (DaemonListener)in.readObject();
} finally {
in.close();
}
String childpid = null;
childpid = l.startLoop();
System.out.println("Started daemon " + daemonName + "." + (null == childpid ? "" : " PID " + childpid));
Log.info("Started daemon " + daemonName + "." + (null == childpid ? "" : " PID " + childpid));
// To log output at this level we have to keep running until the daemon exits
if (outputFile != null) {
boolean interrupted = false;
do {
try {
child.waitFor();
} catch (InterruptedException e) {interrupted = true;}
} while (!interrupted);
}
if (null != output)
output.close();
}
protected static void stopDaemon(Daemon daemon, String pid) throws FileNotFoundException, IOException, ClassNotFoundException {
String daemonName = daemon.daemonName();
if (!getRMIFile(daemonName, pid).exists()) {
System.out.println("Daemon " + daemonName + " does not appear to be running.");
Log.info("Daemon " + daemonName + " does not appear to be running.");
return;
}
ScheduledThreadPoolExecutor stopTimer = new ScheduledThreadPoolExecutor(1);
stopTimer.schedule(daemon.new StopTimer(daemonName, pid), SystemConfiguration.SYSTEM_STOP_TIMEOUT, TimeUnit.MILLISECONDS);
ObjectInputStream in = new ObjectInputStream(new FileInputStream(getRMIFile(daemonName, pid)));
DaemonListener l;
try {
l = (DaemonListener)in.readObject();
} finally {
in.close();
}
try {
l.shutDown();
System.out.println("Daemon " + daemonName + " is shut down.");
Log.info("Daemon " + daemonName + " is shut down.");
} catch(RemoteException e) {
cleanupDaemon(daemonName, pid);
}
}
protected static void cleanupDaemon(String daemonName, String pid) throws IOException {
// looks like the RMI file is still here, but the daemon is gone. let's delete the file, then,
System.out.println("Daemon " + daemonName + " seems to have died some other way, cleaning up state...");
Log.info("Daemon " + daemonName + " seems to have died some other way, cleaning up state...");
_daemon.rmRMIFile(pid);
}
protected static void signalDaemon(String daemonName, String sigName, String pid) throws FileNotFoundException, IOException, ClassNotFoundException {
if (!getRMIFile(daemonName, pid).exists()) {
System.out.println("Daemon " + daemonName + " does not appear to be running.");
Log.info("Daemon " + daemonName + " does not appear to be running.");
return;
}
ObjectInputStream in = new ObjectInputStream(new FileInputStream(getRMIFile(daemonName, pid)));
DaemonListener l;
try {
l = (DaemonListener)in.readObject();
} finally {
in.close();
}
try {
if (l.signal(sigName)) {
System.out.println("Signal " + sigName + " delivered.");
Log.info("Signal " + sigName + " delivered.");
} else {
System.out.println("Signal " + sigName + " not delivered: unrecognized or signal failed");
Log.info("Daemon " + daemonName + " not delivered: unrecognized or signal failed.");
}
} catch(RemoteException e) {
// looks like the RMI file is still here, but the daemon is gone. We won't clean up on signal.
System.out.println("Daemon " + daemonName + " seems to have died somehow.");
Log.info("Daemon " + daemonName + " seems to have died somehow.");
}
}
protected static File getRMIFile(String daemonName, String pid) {
String name = ".rmi-server-" + daemonName + (null == pid ? "" : "-" + pid) + ".obj";
return new File(System.getProperty("user.home"), name);
}
/**
* Rename RMI file to add PID if available, otherwise do nothing.
* @param daemonName the name of this daemon
* @param pid the PID to add to the RMI file name or null if unavailable
*/
protected static void renameRMIFile(String daemonName, String pid) {
if (null != pid) {
getRMIFile(daemonName, null).renameTo(getRMIFile(daemonName, pid));
}
}
protected static File getRMITempFile(String daemonName) throws IOException {
String prefix = ".rmi-server-" + daemonName;
return File.createTempFile(prefix, null, new File(System.getProperty("user.home")));
}
/**
* Remove the RMIFile when you don't know the PID
* @throws IOException
*/
protected void rmRMIFile(String pid) throws IOException {
getRMIFile(_daemonName, pid).delete();
}
protected static void runDaemon(Daemon daemon, String args[]) throws IOException {
_daemon = daemon;
Mode mode = Mode.MODE_UNKNOWN;
String sigName = null;
String targetPID = null;
// Argument parsing ONLY here: don't do any execution yet
if (0 == args.length) {
mode = Mode.MODE_INTERACTIVE;
} else if (args[0].equals("-start")) {
mode = Mode.MODE_START;
} else if (args[0].equals("-stop")) {
mode = Mode.MODE_STOP;
if (args.length < 2) {
daemon.usage();
}
targetPID = args[1];
} else if (args[0].equals("-daemon")) {
mode = Mode.MODE_DAEMON;
} else if (args[0].equals("-interactive")) {
mode = Mode.MODE_INTERACTIVE;
} else if (args[0].equals("-signal")) {
mode = Mode.MODE_SIGNAL;
if (args.length < 3) {
daemon.usage();
}
sigName = args[1];
targetPID = args[2];
}
if ("0".equals(targetPID)) {
// This is request to apply to the single instance on platforms where
// PIDs are not available
targetPID = null;
}
// Now proceed based on mode, catching all exceptions
try {
switch (mode) {
case MODE_INTERACTIVE:
String pid = SystemConfiguration.getPID();
daemon.setPid(pid);
daemon.initialize(args, daemon);
Log.info("Running " + daemon.daemonName() + " in the foreground." + (null == pid ? "" : " PID " + pid));
WorkerThread wt = daemon.createWorkerThread();
// Set up remote access when interactive also to enable signals
setupRemoteAccess(daemon, wt);
// In Interactive mode there is no startLoop() invocation to separate daemon
// process, so just rename our RMI file immediately
renameRMIFile(daemon.daemonName(), pid);
wt.start();
wt.join();
System.exit(0);
case MODE_START:
// Don't initialize since this process will not become
// the daemon: this will start a new process
startDaemon(daemon.daemonName(), daemon.getClass().getName(), args);
System.exit(0);
case MODE_STOP:
// Don't initialize since this process will never be the daemon
// This will signal daemon to terminate
stopDaemon(daemon, targetPID);
System.exit(0);
case MODE_DAEMON:
daemon.initialize(args, daemon);
Log.info(daemon.daemonName() + " started in background " + new Date());
// This will create daemon thread and RMI server to receive startLoop command
// from controller process that launched this process
setupRemoteAccess(daemon, null);
break;
case MODE_SIGNAL:
// This will signal daemon to do something specifically named
assert(null != sigName);
signalDaemon(daemon.daemonName(), sigName, targetPID);
break;
default:
daemon.usage();
}
} catch (Exception e) {
Log.warning(e.getClass().getName() + " in daemon startup: " + e.getMessage());
Log.warningStackTrace(e);
if (mode == Mode.MODE_DAEMON) {
// Make sure to terminate if there is an uncaught
// exception trying to run as daemon so that
// it is obvious that something failed because
// process will have gone away despite whatever
// threads may have got started.
System.exit(1);
}
}
Log.info("Daemon runner finished.");
}
public void setInteractive() {
_interactive = true;
}
/**
* Main entry point for command line invocation.
* @param args Arguments passed in from command line.
*/
public static void main(String[] args) {
// Need to override in each subclass to make proper class.
Daemon daemon = null;
try {
daemon = new Daemon();
runDaemon(daemon, args);
} catch (Exception e) {
Log.warning("Error attempting to start daemon.");
Log.warningStackTrace(e);
System.err.println("Error attempting to start daemon.");
}
}
public Object getStatus(String daemonName, String type) throws FileNotFoundException, IOException, ClassNotFoundException {
if (!getRMIFile(daemonName, _pid).exists()) {
System.out.println("Daemon " + daemonName + " does not appear to be running.");
Log.info("Daemon " + daemonName + " does not appear to be running.");
return null;
}
ObjectInputStream in = new ObjectInputStream(new FileInputStream(getRMIFile(daemonName, _pid)));
DaemonListener l;
try {
l = (DaemonListener)in.readObject();
} finally {
in.close();
}
return l.status(type);
}
}