package org.radargun.service; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import org.radargun.logging.Log; import org.radargun.logging.LogFactory; import org.radargun.traits.Killable; import org.radargun.traits.Lifecycle; import org.radargun.utils.TimeService; /** * Java runtime does not provide API that would allow us to manage full process * tree, that's why we delegate the start/stop/kill handling to OS-specific * scripts. So far, only Unix scripts are implemented. * * @author Radim Vansa <rvansa@redhat.com> */ public class ProcessLifecycle<T extends ProcessService> implements Lifecycle, Killable { protected final Log log = LogFactory.getLog(getClass()); protected final T service; protected ProcessOutputReader outputReader, errorReader; private final CopyOnWriteArrayList<Listener> listeners = new CopyOnWriteArrayList<>(); private String prefix; private String extension; private Process process; private String pid; public ProcessLifecycle(T service) { this.service = service; prefix = service.getCommandPrefix(); extension = service.getCommandSuffix(); } @Override public void kill() { fireBeforeStop(false); try { Runnable waiting = killAsyncInternal(); if (waiting == null) return; waiting.run(); } finally { fireAfterStop(false); } } @Override public void killAsync() { fireBeforeStop(false); final Runnable waiting = killAsyncInternal(); if (waiting == null) { fireAfterStop(false); return; } Thread listenerInvoker = new Thread(new Runnable() { @Override public void run() { try { waiting.run(); } finally { fireAfterStop(false); } } }, "StopListenerInvoker"); listenerInvoker.setDaemon(true); listenerInvoker.start(); } protected Runnable killAsyncInternal() { if (!isRunning()) { log.warn("Cannot kill, process is not running"); return null; } try { fireBeforeStop(false); final Process process = new ProcessBuilder().inheritIO() .command(Arrays.asList(prefix + "kill" + extension, getPid())).start(); return new Runnable() { @Override public void run() { for (;;) { try { process.waitFor(); } catch (InterruptedException e) { log.trace("Interrupted waiting for kill", e); } if (!isRunning()) return; } } }; } catch (IOException e) { log.error("Cannot kill service", e); return null; } } @Override public void start() { if (isRunning()) { log.warn("Process is already running"); return; } fireBeforeStart(); try { startInternal(); } finally { fireAfterStart(); } } protected void startInternal() { List<String> command = new ArrayList<String>(); command.addAll(service.getCommand()); Map<String, String> env = service.getEnvironment(); log.info("Environment:\n" + env); log.info("Starting with: " + command); ProcessBuilder pb = new ProcessBuilder().command(command); for (Map.Entry<String, String> envVar : env.entrySet()) { pb.environment().put(envVar.getKey(), envVar.getValue()); } StreamWriter inputWriter = getInputWriter(); StreamReader outputReader = getOutputReader(); StreamReader errorReader = getErrorReader(); if (inputWriter == null) { pb.redirectInput(ProcessBuilder.Redirect.INHERIT); } if (outputReader == null) { pb.redirectOutput(ProcessBuilder.Redirect.INHERIT); } if (errorReader == null) { pb.redirectError(ProcessBuilder.Redirect.INHERIT); } try { process = pb.start(); if (inputWriter != null) inputWriter.setStream(process.getOutputStream()); if (outputReader != null) outputReader.setStream(process.getInputStream()); if (errorReader != null) errorReader.setStream(process.getErrorStream()); setPid(getProcessId(process)); } catch (IOException e) { log.error("Failed to start", e); } } protected String getProcessId(Process process) { Class<?> clazz = process.getClass(); try { if (clazz.getName().equals("java.lang.UNIXProcess")) { Field pidField = clazz.getDeclaredField("pid"); pidField.setAccessible(true); Object value = pidField.get(process); if (value instanceof Integer) { return ((Integer) value).toString(); } } else { throw new IllegalArgumentException("Only unix is supported as OS."); } } catch (SecurityException sx) { sx.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return null; } @Override public void stop() { if (!isRunning()) { log.warn("Process is not running, cannot stop"); return; } fireBeforeStop(true); try { stopInternal(); } finally { fireAfterStop(true); outputReader.interrupt(); errorReader.interrupt(); } } protected void stopInternal() { try { long startTime = TimeService.currentTimeMillis(); for (; ; ) { String command = service.stopTimeout < 0 || TimeService.currentTimeMillis() < startTime + service.stopTimeout ? "stop" : "kill"; Process process = new ProcessBuilder().inheritIO().command(Arrays.asList(prefix + command + extension, getPid())).start(); try { process.waitFor(service.stopTimeout, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { log.trace("Interrupted waiting for stop", e); } if (!isRunning()) return; } } catch (IOException e) { log.error("Cannot stop service", e); } } @Override public boolean isRunning() { return getChildPIDs().length() != 0 || isPIDAlive(pid); } protected synchronized StreamReader getOutputReader() { if (outputReader == null) { outputReader = new ProcessOutputReader(new LineConsumer() { @Override public void consume(String line) { service.reportOutput(line); } }); } return outputReader; } protected synchronized StreamReader getErrorReader() { if (errorReader == null) { errorReader = new ProcessOutputReader(new LineConsumer() { @Override public void consume(String line) { service.reportError(line); } }); } return errorReader; } protected StreamWriter getInputWriter() { return null; } /** * Provides a hook for service to read output */ interface StreamReader { void setStream(InputStream stream); } /** * Provides a hook for passing input to the process */ interface StreamWriter { void setStream(OutputStream stream); } interface LineConsumer { void consume(String line); } protected static class ProcessOutputReader extends Thread implements StreamReader { protected final Log log = LogFactory.getLog(getClass()); protected BufferedReader reader; protected LineConsumer consumer; public ProcessOutputReader(LineConsumer consumer) { this.consumer = consumer; } @Override public void setStream(InputStream stream) { this.reader = new BufferedReader(new InputStreamReader(stream)); if (!isAlive()) { this.start(); } } @Override public void run() { String line; try { while (!isInterrupted() && (line = reader.readLine()) != null) { consumer.consume(line); } } catch (IOException e) { log.error("Failed to read server output", e); } finally { try { reader.close(); } catch (IOException e) { log.error("Failed to close reader", e); } } } } public void addListener(Listener listener) { listeners.add(listener); } public void removeListener(Listener listener) { listeners.remove(listener); } // lambdas, wish you were here... private interface ListenerRunner { void run(Listener listener); } private void fireListeners(ListenerRunner runner) { for (Listener listener : listeners) { try { runner.run(listener); } catch (Exception e) { log.error("Listener has thrown an exception", e); } } } protected void fireBeforeStart() { fireListeners(new ListenerRunner() { @Override public void run(Listener listener) { listener.beforeStart(); } }); } protected void fireAfterStart() { fireListeners(new ListenerRunner() { @Override public void run(Listener listener) { listener.afterStart(); } }); } protected void fireBeforeStop(final boolean graceful) { fireListeners(new ListenerRunner() { @Override public void run(Listener listener) { listener.beforeStop(graceful); } }); } protected void fireAfterStop(final boolean graceful) { fireListeners(new ListenerRunner() { @Override public void run(Listener listener) { listener.afterStop(graceful); } }); } public interface Listener { void beforeStart(); void afterStart(); void beforeStop(boolean graceful); void afterStop(boolean graceful); } public static class ListenerAdapter implements Listener { public void beforeStart() {} public void afterStart() {} public void beforeStop(boolean graceful) {} public void afterStop(boolean graceful) {} } /* * Return a list of the children PIDs for the service */ private String getChildPIDs() { if (getPid() != null) { ProcessBuilder pb = new ProcessBuilder().command("pgrep", "-P", getPid()); pb.redirectError(ProcessBuilder.Redirect.INHERIT); pb.redirectInput(ProcessBuilder.Redirect.INHERIT); try { Process process = pb.start(); StringBuilder sb = new StringBuilder(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { String line; while ((line = reader.readLine()) != null) sb.append(line); } return sb.toString().split(" ")[0].trim(); } catch (IOException e) { log.error("Failed to read service child PIDs", e); return ""; } } return ""; } private boolean isPIDAlive(String pid) { if (pid != null) { ProcessBuilder pb = new ProcessBuilder().command(prefix + "is_alive" + extension, pid); pb.redirectError(ProcessBuilder.Redirect.INHERIT); pb.redirectInput(ProcessBuilder.Redirect.INHERIT); try { Process process = pb.start(); StringBuilder sb = new StringBuilder(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { String line = reader.readLine(); if (line != null && !line.isEmpty()) { return true; } } return false; } catch (IOException e) { log.error("Failed to test if service " + pid + " is running", e); return false; } } return false; } public String getPid() { return pid; } public void setPid(String pid) { this.pid = pid; } }