Java tutorial
/* * Copyright (C) 2011 Laurent Caillette * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation, either * version 3 of the License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.novelang.outfit.shell; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.List; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; import org.apache.commons.lang.StringUtils; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import org.novelang.logger.Logger; import org.novelang.logger.LoggerFactory; /** * Starts and stops a {@link Process}, watching its standard and error outputs. * * Unfortunately the {@link Process} doesn't tell about OS-dependant PID. * There is no chance to kill spawned processes if the VM running {@link ProcessShell} * crashes. * * See good discussion * <a href="http://blog.igorminar.com/2007/03/how-java-application-can-discover-its.html">here</a>. * * @author Laurent Caillette */ public abstract class ProcessShell { private static final Logger LOGGER = LoggerFactory.getLogger(ProcessShell.class); private final File workingDirectory; private final List<String> processArguments; private final Predicate<String> startupSensor; private final String nickname; private final ThreadGroup threadGroup; private Thread standardStreamWatcherThread = null; private Thread errorStreamWatcherThread = null; private Process process = null; private static final ImmutableList<String> NO_PARAMETERS = ImmutableList.of(); protected ProcessShell(final File workingDirectory, final String nickname, final List<String> processArguments, final Predicate<String> startupSensor) { checkArgument(workingDirectory.isDirectory()); this.workingDirectory = workingDirectory; this.processArguments = processArguments == null ? NO_PARAMETERS : processArguments; this.startupSensor = checkNotNull(startupSensor); checkArgument(!StringUtils.isBlank(nickname)); this.nickname = nickname; threadGroup = new ThreadGroup(getClass().getSimpleName() + "-" + nickname); } public String getNickname() { return nickname; } protected final void start(final long timeout, final TimeUnit timeUnit) throws IOException, InterruptedException, ProcessCreationException { final Semaphore startupSemaphore = new Semaphore(0); LOGGER.info("Starting process ", getNickname(), " in directory '", workingDirectory.getAbsolutePath(), "'..."); LOGGER.info("Arguments: ", processArguments); synchronized (stateLock) { ensureInState(State.READY); process = new ProcessBuilder().command(processArguments).directory(workingDirectory).start(); standardStreamWatcherThread = new Thread(threadGroup, createStandardOutputWatcher(process.getInputStream(), startupSemaphore), "standardWatcher-" + nickname); errorStreamWatcherThread = new Thread(threadGroup, createErrorOutputWatcher(process.getErrorStream()), "errorWatcher-" + nickname); standardStreamWatcherThread.setDaemon(true); standardStreamWatcherThread.start(); errorStreamWatcherThread.setDaemon(true); errorStreamWatcherThread.start(); LOGGER.debug("Waiting for startup sensor to detect startup line..."); startupSemaphore.tryAcquire(1, timeout, timeUnit); if (state == State.BROKEN) { throw new ProcessCreationException("Couldn't create " + getNickname()); } else { state = State.RUNNING; } } LOGGER.info("Successfully launched process: ", getNickname(), " (it may be initializing now)."); } private InputStreamWatcher createStandardOutputWatcher(final InputStream standardOutput, final Semaphore startupSemaphore) { return new InputStreamWatcher(standardOutput) { @Override protected void interpretLine(final String line) { if (line != null) { LOGGER.debug("Standard output from ", getNickname(), ": >>> ", line); if ( /*startupSemaphore.availablePermits() == 0 &&*/ startupSensor.apply(line)) { LOGGER.debug("Startup detected for ", getNickname(), "."); startupSemaphore.release(); } } } @Override protected void handleThrowable(final Throwable throwable) { handleThrowableFromProcess(throwable); } }; } private InputStreamWatcher createErrorOutputWatcher(final InputStream standardError) { return new InputStreamWatcher(standardError) { @Override protected void interpretLine(final String line) { if (line != null) { LOGGER.warn("Error from ", getNickname(), ": >>> ", line); } } @Override protected void handleThrowable(final Throwable throwable) { handleThrowableFromProcess(throwable); } }; } private void handleThrowableFromProcess(final Throwable throwable) { synchronized (stateLock) { if (state != State.SHUTTINGDOWN && state != State.READY // Makes sense if a complete shutdown just occured. ) { state = State.BROKEN; } } LOGGER.error("Throwable caught while reading supervised process stream in ", getNickname(), throwable); } /** * Requests to shut the process down. This method is not aware if the process was alread down. */ protected final Integer shutdownProcess(final boolean force) throws InterruptedException { Integer exitCode = null; synchronized (stateLock) { try { if (state == State.RUNNING) { state = State.SHUTTINGDOWN; if (force) { interruptWatcherThreads(); process.destroy(); } else { exitCode = process.waitFor(); interruptWatcherThreads(); } } } finally { process = null; standardStreamWatcherThread = null; errorStreamWatcherThread = null; state = State.READY; //TERMINATED ; } } LOGGER.debug("Process shutdown ended for ", getNickname(), ", returning ", exitCode, "."); return exitCode; } private void interruptWatcherThreads() { standardStreamWatcherThread.interrupt(); errorStreamWatcherThread.interrupt(); } protected final Object stateLock = new Object(); private State state = State.READY; /** * Synchronization left to caller. */ protected final void ensureInState(final State expected, final State... otherExpected) { if (state != expected) { for (final State other : otherExpected) { if (state == other) { return; } } throw new IllegalStateException("Expected to be in state " + expected + " but was in " + state); } } private enum State { READY, RUNNING, BROKEN, SHUTTINGDOWN /*, TERMINATED*/ } }