/* * 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(); } }); } }