/*
* Copyright 2010-2013 the original author or authors.
*
* 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.
*/
package org.springframework.cloud.stream.modules.test.gemfire.process;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.springframework.cloud.stream.modules.test.gemfire.process.support.PidUnavailableException;
import org.springframework.cloud.stream.modules.test.gemfire.process.support.ProcessUtils;
import org.springframework.cloud.stream.modules.test.gemfire.support.FileSystemUtils;
import org.springframework.cloud.stream.modules.test.gemfire.support.IOUtils;
import org.springframework.cloud.stream.modules.test.gemfire.support.ThreadUtils;
import org.springframework.cloud.stream.modules.test.gemfire.support.ThrowableUtils;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
/**
* The ProcessWrapper class is a wrapper for a Process object representing an OS process and the ProcessBuilder used
* to construct and start the process.
* @author John Blum
* @see java.lang.Process
* @see java.lang.ProcessBuilder
* @since 1.5.0
*/
public class ProcessWrapper {
protected static final boolean DEFAULT_DAEMON_THREAD = true;
protected static final long DEFAULT_WAIT_TIME_MILLISECONDS = TimeUnit.SECONDS.toMillis(15);
private final List<ProcessInputStreamListener> listeners = new CopyOnWriteArrayList<ProcessInputStreamListener>();
protected final Logger log = Logger.getLogger(getClass().getName());
private final Process process;
private final ProcessConfiguration processConfiguration;
public ProcessWrapper(final Process process, final ProcessConfiguration processConfiguration) {
Assert.notNull(process, "The Process object backing this wrapper must not be null!");
Assert.notNull(processConfiguration, "The context and configuration meta-data providing details about"
+ " the environment in which the process is running and how the process was configured must not be " +
"null!");
this.process = process;
this.processConfiguration = processConfiguration;
init();
}
private void init() {
newThread("Process OUT Stream Reader", newProcessInputStreamReader(process.getInputStream())).start();
if (!isRedirectingErrorStream()) {
newThread("Process ERR Stream Reader", newProcessInputStreamReader(process.getErrorStream())).start();
}
}
protected Runnable newProcessInputStreamReader(final InputStream in) {
return new Runnable() {
@Override
public void run() {
if (isRunning()) {
BufferedReader inputReader = new BufferedReader(new InputStreamReader(in));
try {
for (String input = inputReader.readLine(); input != null; input = inputReader.readLine()) {
for (ProcessInputStreamListener listener : listeners) {
listener.onInput(input);
}
}
}
catch (IOException ignore) {
// ignore IO error and just stop reading from the process input stream
// IO error occurred most likely because the process was terminated
}
finally {
IOUtils.close(inputReader);
}
}
}
};
}
protected Thread newThread(final String name, final Runnable task) {
Assert.isTrue(!StringUtils.isEmpty(name), "The name of the Thread must be specified!");
Assert.notNull(task, "The Thread task must not be null!");
Thread thread = new Thread(task, name);
thread.setDaemon(DEFAULT_DAEMON_THREAD);
thread.setPriority(Thread.NORM_PRIORITY);
return thread;
}
public List<String> getCommand() {
return processConfiguration.getCommand();
}
public String getCommandString() {
return processConfiguration.getCommandString();
}
public Map<String, String> getEnvironment() {
return processConfiguration.getEnvironment();
}
public int getPid() {
return ProcessUtils.findAndReadPid(getWorkingDirectory());
}
public int safeGetPid() {
try {
return getPid();
}
catch (PidUnavailableException ignore) {
return -1;
}
}
public boolean isRedirectingErrorStream() {
return processConfiguration.isRedirectingErrorStream();
}
public boolean isRunning() {
return ProcessUtils.isRunning(this.process);
}
public File getWorkingDirectory() {
return processConfiguration.getWorkingDirectory();
}
public int exitValue() {
return process.exitValue();
}
public int safeExitValue() {
try {
return exitValue();
}
catch (IllegalThreadStateException ignore) {
return -1;
}
}
public String readLogFile() throws IOException {
File[] logFiles = FileSystemUtils.listFiles(getWorkingDirectory(), new FileFilter() {
@Override
public boolean accept(final File pathname) {
return (pathname != null && (pathname.isDirectory() || pathname.getAbsolutePath().endsWith(".log")));
}
});
if (logFiles.length > 0) {
return readLogFile(logFiles[0]);
}
else {
throw new FileNotFoundException(String.format(
"No log files were found in the process's working directory (%1$s)!", getWorkingDirectory()));
}
}
public String readLogFile(final File log) throws IOException {
return FileCopyUtils.copyToString(new BufferedReader(new FileReader(log)));
}
public boolean register(final ProcessInputStreamListener listener) {
return (listener != null && listeners.add(listener));
}
public void registerShutdownHook() {
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
shutdown();
}
}));
}
public void signalStop() {
try {
ProcessUtils.signalStop(this.process);
}
catch (IOException e) {
log.warning("Failed to signal the process to stop!");
if (log.isLoggable(Level.FINE)) {
log.fine(ThrowableUtils.toString(e));
}
}
}
public int stop() {
return stop(DEFAULT_WAIT_TIME_MILLISECONDS);
}
public int stop(final long milliseconds) {
if (isRunning()) {
int exitValue = -1;
final int pid = safeGetPid();
final long timeout = (System.currentTimeMillis() + milliseconds);
final AtomicBoolean exited = new AtomicBoolean(false);
ExecutorService executorService = Executors.newSingleThreadExecutor();
try {
Future<Integer> futureExitValue = executorService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
process.destroy();
int exitValue = process.waitFor();
exited.set(true);
return exitValue;
}
});
while (!exited.get() && System.currentTimeMillis() < timeout) {
try {
exitValue = futureExitValue.get(milliseconds, TimeUnit.MILLISECONDS);
log.info(String.format("Process [%1$s] has been stopped.%n", pid));
}
catch (InterruptedException ignore) {
}
}
}
catch (TimeoutException e) {
exitValue = -1;
log.warning(String.format("Process [%1$d] did not stop within the allotted timeout of %2$d seconds.%n",
pid, TimeUnit.MILLISECONDS.toSeconds(milliseconds)));
}
catch (Exception ignore) {
// handles CancellationException, ExecutionException
}
finally {
executorService.shutdownNow();
}
return exitValue;
}
else {
return exitValue();
}
}
public int shutdown() {
if (isRunning()) {
log.info(String.format("Stopping process [%1$d]...%n", safeGetPid()));
signalStop();
waitFor();
}
return stop();
}
public boolean unregister(final ProcessInputStreamListener listener) {
return listeners.remove(listener);
}
public void waitFor() {
waitFor(DEFAULT_WAIT_TIME_MILLISECONDS);
}
public void waitFor(final long milliseconds) {
ThreadUtils.timedWait(milliseconds, 500, new ThreadUtils.WaitCondition() {
@Override
public boolean waiting() {
return isRunning();
}
});
}
}