/*
* 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*/ }
}