Java tutorial
/* * #%L * MariaDB4j * %% * Copyright (C) 2012 - 2014 Michael Vorburger * %% * Licensed 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. * #L% */ package com.lenox.common.exec; import org.apache.commons.exec.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; import java.util.Map; /** * Managed OS Process (Executable, Program, Command). * Created by {@link ManagedProcessBuilder#build()}. * <p/> * Intended for controlling external "tools", often "daemons", which produce some text-based control output. * In this form not yet suitable for programs returning binary data via stdout (but could be extended). * <p/> * Does reasonably extensive logging about what it's doing (contrary to Apache Commons Exec), * including logging the processes stdout & stderr, into SLF4J (not the System.out.Console). * * @author Michael Vorburger * @see org.apache.commons.exec.Executor * Internally based on http://commons.apache.org/exec/ but intentionally not exposing this; could be switched later, if there is any need. */ public class ManagedProcess { private static final Logger logger = LoggerFactory.getLogger(ManagedProcess.class); private static final int INVALID_EXITVALUE = Executor.INVALID_EXITVALUE; private final CommandLine commandLine; private final Executor executor = new DefaultExecutor(); private final DefaultExecuteResultHandler resultHandler = new LoggingExecuteResultHandler(); private final ExecuteWatchdog watchDog = new ExecuteWatchdog(ExecuteWatchdog.INFINITE_TIMEOUT); private final ProcessDestroyer shutdownHookProcessDestroyer = new LoggingShutdownHookProcessDestroyer(); private final Map<String, String> environment; private File workingDir; private final InputStream input; private final boolean destroyOnShutdown; private final int consoleBufferMaxLines; private boolean isAlive = false; private String procShortName; private RollingLogOutputStream console; private MultiOutputStream stdouts; private MultiOutputStream stderrs; private Process process; private String argsAsString; /** * Package local constructor. * <p/> * Keep ch.vorburger.exec's API separate from Apache Commons Exec, so it COULD be replaced. * * @param commandLine Apache Commons Exec CommandLine * @param directory Working directory, or null * @param environment Environment Variable. * @see ManagedProcessBuilder#build() */ ManagedProcess(CommandLine commandLine, File directory, Map<String, String> environment, InputStream input, boolean destroyOnShutdown, int consoleBufferMaxLines) { this.commandLine = commandLine; this.environment = environment; if (input != null) { this.input = buffer(input); } else { this.input = null; // this is safe/OK/expected; PumpStreamHandler constructor handles this as expected } if (directory != null) { executor.setWorkingDirectory(directory); this.workingDir = directory; } executor.setWatchdog(watchDog); this.destroyOnShutdown = destroyOnShutdown; this.consoleBufferMaxLines = consoleBufferMaxLines; } public CommandLine getCommandLine() { return commandLine; } private int chmod(String filename, int mode) { try { Class<?> fspClass = Class.forName("java.util.prefs.FileSystemPreferences"); Method chmodMethod = fspClass.getDeclaredMethod("chmod", String.class, Integer.TYPE); chmodMethod.setAccessible(true); return (Integer) chmodMethod.invoke(null, filename, mode); } catch (Throwable ex) { return -1; } } // stolen from commons-io IOUtiles (@since v2.5) protected BufferedInputStream buffer(final InputStream inputStream) { // reject null early on rather than waiting for IO operation to fail if (inputStream == null) { // not checked by BufferedInputStream throw new NullPointerException("inputStream == null"); } return inputStream instanceof BufferedInputStream ? (BufferedInputStream) inputStream : new BufferedInputStream(inputStream); } public File getWorkingDir() { return workingDir; } public void setWorkingDir(File workingDir) { this.workingDir = workingDir; } /** * Starts the Process. * <p/> * This method always immediately returns (i.e. launches the process asynchronously). * Use the different waitFor... methods if you want to "block" on the spawned process. * * @throws ManagedProcessException if the process could not be started */ public synchronized void start() throws ManagedProcessException, IOException, InterruptedException { startPreparation(); startWithThread(); } public Process getProcess() { return process; } public void startWithThread() throws IOException, InterruptedException { StringBuilder builder = new StringBuilder(); for (String arg : commandLine.getArguments()) { if (builder.length() > 0) { builder.append(" "); } builder.append(arg); } String args = builder.toString().replaceAll("\"", ""); try { Runtime runtime = Runtime.getRuntime(); if (workingDir == null) { logger.debug("Starting Process with no working dir : {} ", commandLine.getExecutable() + " " + args); argsAsString = commandLine.getExecutable() + " " + args; process = runtime.exec(argsAsString); logger.debug("Started Process with no working dir : {} ", commandLine.getExecutable() + " " + args); } else { try { logger.debug("Changing permissions on process with working dir : {} ", workingDir.getAbsolutePath() + File.separator + commandLine.getExecutable()); runtime.exec("chmod +rx " + workingDir.getAbsolutePath() + File.separator + commandLine.getExecutable()); } catch (Exception e) { logger.error("Error changing permissions on process : ", e); throw e; } try { logger.debug("Starting Process with working dir : {}", commandLine.getExecutable() + " " + args); process = runtime.exec(commandLine.getExecutable() + " " + args, null, workingDir); } catch (Exception e) { logger.error("Error starting process with working dir : ", e); throw e; } logger.debug("Started Process: {} ", workingDir.getAbsolutePath() + File.separator + commandLine.getExecutable()); } } catch (Exception e) { logger.error("Error starting process : ", e); throw e; } } protected synchronized void startPreparation() throws ManagedProcessException { if (isAlive()) { throw new ManagedProcessException(procLongName() + " is still running, use another ManagedProcess instance to launch another one"); } if (logger.isInfoEnabled()) logger.info("Starting {}", procLongName()); stdouts = new MultiOutputStream(); stderrs = new MultiOutputStream(); PumpStreamHandler outputHandler = new PumpStreamHandler(stdouts, stderrs, input); executor.setStreamHandler(outputHandler); String pid = procShortName(); stdouts.addOutputStream(new SLF4jLogOutputStream(logger, pid, SLF4jLogOutputStream.Type.stdout)); stderrs.addOutputStream(new SLF4jLogOutputStream(logger, pid, SLF4jLogOutputStream.Type.stderr)); if (consoleBufferMaxLines > 0) { console = new RollingLogOutputStream(consoleBufferMaxLines); stdouts.addOutputStream(console); stderrs.addOutputStream(console); } if (destroyOnShutdown) { executor.setProcessDestroyer(shutdownHookProcessDestroyer); } if (commandLine.isFile()) { try { Util.forceExecutable(getExecutableFile()); } catch (Exception e) { throw new ManagedProcessException("Unable to make command executable", e); } } } public File getExecutableFile() { return new File(commandLine.getExecutable()); } protected synchronized void startExecute() throws ManagedProcessException { try { executor.execute(commandLine, environment, resultHandler); } catch (Exception e) { throw new ManagedProcessException("Launch failed: " + commandLine, e); } isAlive = true; // We now must give the system a say 100ms chance to run the background // thread now, otherwise the resultHandler in checkResult() won't work. // // This is admittedly not ideal, but to do better would require significant // changes to DefaultExecutor, so that its execute() would "fail fast" and // throw an Exception immediately if process start-up fails by doing the // launch in the current thread, and then spawns a separate thread only // for the waitFor(). // // As DefaultExecutor doesn't seem to have been written with extensibility // in mind, and rewriting it to start gain 100ms (at the start of every process..) // doesn't seem to be worth it for now, I'll leave it like this, for now. // try { this.wait(100); // better than Thread.sleep(100); -- thank you, FindBugs } catch (InterruptedException e) { throw handleInterruptedException(e); } checkResult(); } /** * Starts the Process and waits (blocks) until the process prints a certain message. * <p/> * You should be sure that the process either prints this message at some * point, or otherwise exits on it's own. This method will otherwise be * slow, but never block forever, as it will "give up" and always return * after max. maxWaitUntilReturning ms. * * @param messageInConsole text to wait for in the STDOUT/STDERR of the external process * @param maxWaitUntilReturning maximum time to wait, in milliseconds, until returning, if message wasn't seen * @return true if message was seen in console; false if message didn't occur and we're returning due to max. wait timeout * @throws ManagedProcessException for problems such as if the process already exited (without the message ever appearing in the Console) */ public boolean startAndWaitForConsoleMessageMaxMs(String messageInConsole, long maxWaitUntilReturning) throws ManagedProcessException { startPreparation(); CheckingConsoleOutputStream checkingConsoleOutputStream = new CheckingConsoleOutputStream(messageInConsole); if (stdouts != null && stderrs != null) { stdouts.addOutputStream(checkingConsoleOutputStream); stderrs.addOutputStream(checkingConsoleOutputStream); } long timeAlreadyWaited = 0; final int SLEEP_TIME_MS = 50; logger.info("Thread will wait for \"{}\" to appear in Console output of process {} for max. " + maxWaitUntilReturning + "ms", messageInConsole, procLongName()); startExecute(); try { while (!checkingConsoleOutputStream.hasSeenIt() && isAlive()) { try { Thread.sleep(SLEEP_TIME_MS); } catch (InterruptedException e) { throw handleInterruptedException(e); } timeAlreadyWaited += SLEEP_TIME_MS; if (timeAlreadyWaited > maxWaitUntilReturning) { logger.warn("Timed out waiting for \"\"{}\"\" after {}ms (returning false)", messageInConsole, maxWaitUntilReturning); return false; } } // If we got out of the while() loop due to !isAlive() instead of messageInConsole, then throw the same exception as above! if (!checkingConsoleOutputStream.hasSeenIt()) { throw new ManagedProcessException(getUnexpectedExitMsg(messageInConsole)); } else { return true; } } finally { if (stdouts != null && stderrs != null) { stdouts.removeOutputStream(checkingConsoleOutputStream); stderrs.removeOutputStream(checkingConsoleOutputStream); } } } protected String getUnexpectedExitMsg(String messageInConsole) { return "Asked to wait for \"" + messageInConsole + "\" from " + procLongName() + ", but it already exited! (without that message in console)" + getLastConsoleLines(); } protected ManagedProcessException handleInterruptedException(InterruptedException e) throws ManagedProcessException { // TODO Not sure how to best handle this... opinions welcome (see also below) final String message = "Huh?! InterruptedException should normally never happen here..." + procLongName(); logger.error(message, e); return new ManagedProcessException(message, e); } protected void checkResult() throws ManagedProcessException { if (resultHandler.hasResult()) { // We already terminated (or never started) ExecuteException e = resultHandler.getException(); if (e != null) { logger.error(procLongName() + " failed"); throw new ManagedProcessException( procLongName() + " failed, exitValue=" + exitValue() + getLastConsoleLines(), e); } } } /** * Kills the Process. If you expect that the process may not be running anymore, use if ({@link #isAlive()}) around this. If you expect * that the process should still be running at this point, call as is - and it will tell if it had nothing to destroy. * * @throws ManagedProcessException if the Process is already stopped (either because destroy() already explicitly called, or it terminated by itself, or it * was never started) */ public void destroy() throws ManagedProcessException { // // if destroy() is ever giving any trouble, the org.openqa.selenium.os.ProcessUtils may be of interest // if (!isAlive) { throw new ManagedProcessException(procLongName() + " was already stopped (or never started)"); } if (logger.isDebugEnabled()) logger.debug("Going to destroy {}", procLongName()); watchDog.destroyProcess(); try { // Safer to waitFor() after destroy() resultHandler.waitFor(); } catch (InterruptedException e) { throw handleInterruptedException(e); } if (logger.isInfoEnabled()) logger.info("Successfully destroyed {}", procLongName()); isAlive = false; } // Java Doc shamelessly copy/pasted from java.lang.Thread#isAlive() : /** * Tests if this process is alive. * A process is alive if it has been started and has not yet terminated. * * @return <code>true</code> if this process is alive; * <code>false</code> otherwise. */ public boolean isAlive() { // NOPE: return !resultHandler.hasResult(); return isAlive; } /** * Returns the exit value for the subprocess. * * @return the exit value of the subprocess represented by this * <code>Process</code> object. by convention, the value * <code>0</code> indicates normal termination. * @throws ManagedProcessException if the subprocess represented * by this <code>ManagedProcess</code> object has not yet terminated. */ public int exitValue() throws ManagedProcessException { try { return process.exitValue(); } catch (IllegalStateException e) { throw new ManagedProcessException("Exit Value not (yet) available for " + procLongName(), e); } } /** * Waits for the process to terminate. * <p/> * Returns immediately if the process is already stopped (either because * destroy() was already explicitly called, or it terminated by itself). * <p/> * Note that if the process was attempted to be started but that start * failed (may be because the executable could not be found, or some * underlying OS error) then it throws a ManagedProcessException. * <p/> * It also throws a ManagedProcessException if {@link #start()} was never * even called. * * @return exit value (or INVALID_EXITVALUE if {@link #destroy()} was used) * @throws ManagedProcessException see above */ public int waitForExit() throws ManagedProcessException { logger.info("Thread is now going to wait for this process to terminate itself: {}", procLongName()); return waitForExitMaxMsWithoutLog(-1); } /** * Like {@link #waitForExit()}, but waits max. maxWaitUntilReturning, then * returns (even if still running, taking no action). * * @param maxWaitUntilReturning Time to wait * @return exit value, or INVALID_EXITVALUE if the timeout was reached, or * if {@link #destroy()} was used * @throws ManagedProcessException see above */ public int waitForExitMaxMs(long maxWaitUntilReturning) throws ManagedProcessException { logger.info("Thread is now going to wait max. {}ms for process to terminate itself: {}", maxWaitUntilReturning, procLongName()); return waitForExitMaxMsWithoutLog(maxWaitUntilReturning); } protected int waitForExitMaxMsWithoutLog(long maxWaitUntilReturning) throws ManagedProcessException { assertWaitForIsValid(); try { if (maxWaitUntilReturning != -1) { resultHandler.waitFor(maxWaitUntilReturning); checkResult(); if (!isAlive()) return exitValue(); return INVALID_EXITVALUE; } resultHandler.waitFor(); checkResult(); return exitValue(); } catch (InterruptedException e) { throw handleInterruptedException(e); } } /** * Like {@link #waitForExit()}, but waits max. maxWaitUntilReturning, then destroys if still running, and returns. * * @param maxWaitUntilDestroyTimeout Time to wait * @throws ManagedProcessException see above */ public void waitForExitMaxMsOrDestroy(long maxWaitUntilDestroyTimeout) throws ManagedProcessException { waitForExitMaxMs(maxWaitUntilDestroyTimeout); if (isAlive()) { logger.info("Process didn't exit within max. {}ms, so going to destroy it now: {}", maxWaitUntilDestroyTimeout, procLongName()); destroy(); } } protected void assertWaitForIsValid() throws ManagedProcessException { if (!isAlive() && !resultHandler.hasResult()) { throw new ManagedProcessException( "Asked to waitFor " + procLongName() + ", but it was never even start()'ed!"); } } // --- public String getConsole() { if (console != null) return console.getRecentLines(); else return ""; } public String getLastConsoleLines() { return ", last " + consoleBufferMaxLines + " lines of console:\n" + getConsole(); } // --- private String procShortName() { // could later be extended to some sort of fake numeric PID, e.g. "mysqld-1", from a static Map<String execName, Integer id) if (procShortName == null) { File exec = getExecutableFile(); procShortName = exec.getName(); } return procShortName; } private String procLongName() { return "Program " + commandLine.toString() + (executor.getWorkingDirectory() == null ? "" : " (in working directory " + executor.getWorkingDirectory().getAbsolutePath() + ")"); } // --- public class LoggingExecuteResultHandler extends DefaultExecuteResultHandler { @Override public void onProcessComplete(int exitValue) { super.onProcessComplete(exitValue); logger.info(procLongName() + " just exited, with value " + exitValue); isAlive = false; } @Override public void onProcessFailed(ExecuteException e) { super.onProcessFailed(e); if (!watchDog.killedProcess()) { logger.error(procLongName() + " failed unexpectedly", e); } isAlive = false; } } public static class LoggingShutdownHookProcessDestroyer extends ShutdownHookProcessDestroyer { @Override public void run() { logger.info("Shutdown Hook: JVM is about to exit! Going to kill destroyOnShutdown processes..."); super.run(); } } }