/** (C) Copyright 2011-2014 Chiral Behaviors, All Rights Reserved
*
* 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 com.hellblazer.process.impl;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.io.input.ReversedLinesFileReader;
import org.apache.commons.io.input.Tailer;
import org.apache.commons.io.input.TailerListener;
import com.hellblazer.process.CannotStopProcessException;
import com.hellblazer.process.ManagedProcess;
import com.hellblazer.utils.Utils;
/**
* @author Hal Hildebrand
*
*/
abstract public class AbstractManagedProcess implements ManagedProcess,
Cloneable {
public static final String CONTROL_DIR_PREFIX = ".control-";
public static final int DEFAULT_KILL_TIMEOUT_SECONDS = 10;
public static final int DEFAULT_PAUSE_MILLIS = 500;
public static final int MAX_TAIL_BUFFER_LINES = 4000;
private static final Logger log = Logger.getLogger(AbstractManagedProcess.class.getCanonicalName());
private static final long serialVersionUID = 1L;
public static UUID getIdFrom(File homeDirectory) {
if (!homeDirectory.exists() || !homeDirectory.isDirectory()) {
return null;
}
File[] contents = homeDirectory.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.startsWith(CONTROL_DIR_PREFIX);
}
});
if (contents == null || contents.length == 0) {
if (log.isLoggable(Level.FINE)) {
log.fine("Home directory does not exist or does not contain a valid control directory: "
+ homeDirectory.getAbsolutePath());
}
return null;
}
if (contents.length > 1) {
if (log.isLoggable(Level.FINE)) {
log.fine("Home directory contains more than a single control directory: "
+ homeDirectory.getAbsolutePath());
}
return null;
}
String uuidString = contents[0].getName().substring(CONTROL_DIR_PREFIX.length());
if (uuidString.length() == 0) {
if (log.isLoggable(Level.FINE)) {
log.fine("Home directory does not contain a valid control directory: "
+ homeDirectory.getAbsolutePath());
}
return null;
}
return UUID.fromString(uuidString);
}
public static void initializeDirectory(File directory) throws IOException {
remove(directory);
if (!directory.mkdirs()) {
throw new IOException("Cannot create directory: " + directory);
}
}
public static void remove(File directory) throws IOException {
Utils.clean(directory);
if (directory.exists() && !directory.delete()) {
throw new IOException(String.format("Cannot delete %s",
directory.getAbsolutePath()));
}
}
protected List<String> command = new ArrayList<String>();
protected File controlDirectory;
protected File directory;
protected Map<String, String> environment;
protected final UUID id;
protected volatile boolean terminated = false;
public AbstractManagedProcess() {
this(UUID.randomUUID());
}
public AbstractManagedProcess(UUID id) {
this.id = id;
}
abstract public void acquireFromHome(File homeDirectory);
@Override
public void addCommand(String piece) {
if (command == null) {
command = new ArrayList<String>();
}
command.add(piece);
}
@Override
public AbstractManagedProcess clone() {
AbstractManagedProcess clone;
try {
clone = getClass().newInstance();
} catch (InstantiationException e) {
throw new IllegalStateException("cannot create instance", e);
} catch (IllegalAccessException e) {
throw new IllegalStateException(
"cannot create instance due to access restrictions",
e);
}
clone.command = command;
clone.directory = directory;
if (environment != null) {
clone.environment = new HashMap<String, String>();
clone.environment.putAll(environment);
}
return clone;
}
@Override
public ManagedProcess configureFrom(ManagedProcess process) {
command = process.getCommand();
environment = process.getEnvironment();
directory = process.getDirectory();
return this;
}
@Override
public synchronized void destroy() throws IOException {
stop();
remove(directory);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final AbstractManagedProcess other = (AbstractManagedProcess) obj;
if (id == null) {
if (other.id != null) {
return false;
}
} else if (!id.equals(other.id)) {
return false;
}
return true;
}
@Override
public List<String> getCommand() {
return command;
}
@Override
public File getDirectory() {
return directory;
}
@Override
public Map<String, String> getEnvironment() {
return environment;
}
@Override
public UUID getId() {
return id;
}
@Override
public InputStream getStdErr() {
try {
return new FileInputStream(getStdErrFile());
} catch (FileNotFoundException e) {
throw new IllegalThreadStateException(
"Process has not been started");
}
}
@Override
public String getStdErrTail(int numLines) throws IOException {
if (!getStdErrFile().exists()) {
throw new IllegalThreadStateException(
"Process has not been started or has already exited");
}
List<String> lines = new ArrayList<>();
try (ReversedLinesFileReader reader = new ReversedLinesFileReader(
getStdErrFile())) {
int linesRead = 0;
String line;
while (((line = reader.readLine()) != null)
&& (linesRead++ < numLines)) {
lines.add(line);
}
}
Collections.reverse(lines);
StringBuilder builder = new StringBuilder();
for (String line : lines) {
builder.append(line);
builder.append('\n');
}
return builder.toString();
}
@Override
public OutputStream getStdIn() {
try {
return new FileOutputStream(getStdInFile(), true);
} catch (FileNotFoundException e) {
throw new IllegalThreadStateException(
"Process has not been started or has already exited");
}
}
@Override
public InputStream getStdOut() {
try {
return new FileInputStream(getStdOutFile());
} catch (FileNotFoundException e) {
throw new IllegalThreadStateException(
"Process has not been started");
}
}
@Override
public String getStdOutTail(int numLines) throws IOException {
if (!getStdOutFile().exists()) {
throw new IllegalThreadStateException(
"Process has not been started or has already exited");
}
List<String> lines = new ArrayList<>();
try (ReversedLinesFileReader reader = new ReversedLinesFileReader(
getStdOutFile())) {
int linesRead = 0;
String line;
while (((line = reader.readLine()) != null)
&& (linesRead++ < numLines)) {
lines.add(line);
}
}
Collections.reverse(lines);
StringBuilder builder = new StringBuilder();
for (String line : lines) {
builder.append(line);
builder.append('\n');
}
return builder.toString();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (id == null ? 0 : id.hashCode());
return result;
}
@Override
public boolean isSameConfiguration(ManagedProcess other) {
if (command == null) {
if (other.getCommand() != null) {
return false;
}
} else if (!command.equals(other.getCommand())) {
return false;
}
if (directory == null) {
if (other.getDirectory() != null) {
return false;
}
} else if (!directory.equals(other.getDirectory())) {
return false;
}
if (environment == null) {
if (other.getEnvironment() != null) {
return false;
}
} else if (!environment.equals(other.getEnvironment())) {
return false;
}
return true;
}
@Override
public synchronized void restart() throws IOException {
restart(DEFAULT_KILL_TIMEOUT_SECONDS);
}
@Override
public synchronized void restart(int waitForSeconds) throws IOException {
stop();
start();
}
@Override
public void setCommand(List<String> command) {
if (command == null) {
command = new ArrayList<String>();
}
this.command = command;
}
@Override
public void setCommand(String[] command) {
if (command == null) {
command = new String[0];
}
ArrayList<String> commandList = new ArrayList<String>();
for (String part : command) {
commandList.add(part);
}
setCommand(commandList);
}
@Override
public void setDirectory(File directory) {
this.directory = directory;
}
@Override
public void setDirectory(String directory) {
if (directory == null) {
setDirectory((File) null);
}
setDirectory(new File(directory));
}
@Override
public void setEnvironment(Map<String, String> environment) {
if (environment == null) {
environment = new HashMap<String, String>();
}
this.environment = environment;
}
@Override
public synchronized void start() throws IOException {
if (isActive()) {
return;
}
terminated = false;
if (command == null || command.size() == 0) {
command = new ArrayList<String>();
return;
}
if (directory == null) {
throw new IllegalStateException(
"Process home directory must not be null");
}
initializeDirectory(new File(directory, CONTROL_DIR_PREFIX + id));
// Create initial STD IN file
File stdInFile = getStdInFile();
FileOutputStream stdIn = new FileOutputStream(stdInFile);
stdIn.close();
if (log.isLoggable(Level.FINE)) {
log.fine("[" + id + "] executing: " + command + " dir: "
+ directory + " env: " + environment);
}
execute();
// On Windows platforms, the stdout and stderr files might not be
// established yet, so poll
int counter = 0;
while (!getStdErrFile().exists() || !getStdOutFile().exists()) {
try {
Thread.sleep(10);
if (counter++ > 150) {
throw new IOException("Process did not start up correctly");
}
} catch (InterruptedException e) {
return;
}
}
}
@Override
public synchronized void stop() throws CannotStopProcessException {
stop(DEFAULT_KILL_TIMEOUT_SECONDS);
}
/* (non-Javadoc)
* @see com.hellblazer.process.ManagedProcess#tailStdErr(org.apache.commons.io.input.TailerListener)
*/
@Override
public Tailer tailStdErr(TailerListener listener) {
return Tailer.create(getStdErrFile(), listener);
}
/* (non-Javadoc)
* @see com.hellblazer.process.ManagedProcess#tailStdErr(org.apache.commons.io.input.TailerListener)
*/
@Override
public Tailer tailStdErr(TailerListener listener, long delayMillis,
boolean end, boolean reOpen, int bufSize) {
return Tailer.create(getStdErrFile(), listener, delayMillis, end,
reOpen, bufSize);
}
/* (non-Javadoc)
* @see com.hellblazer.process.ManagedProcess#tailStdOut(org.apache.commons.io.input.TailerListener)
*/
@Override
public Tailer tailStdOut(TailerListener listener) {
return Tailer.create(getStdOutFile(), listener);
}
/* (non-Javadoc)
* @see com.hellblazer.process.ManagedProcess#tailStdOut(org.apache.commons.io.input.TailerListener)
*/
@Override
public Tailer tailStdOut(TailerListener listener, long delayMillis,
boolean end, boolean reOpen, int bufSize) {
return Tailer.create(getStdOutFile(), listener, delayMillis, end,
reOpen, bufSize);
}
@Override
public String toString() {
StringBuffer buf = new StringBuffer();
String name = getClass().getCanonicalName();
name = name.substring(name.lastIndexOf('.') + 1);
buf.append(name);
buf.append("{").append(getId()).append("} ");
buf.append(" home dir: ");
buf.append(directory);
buf.append(" pid: ");
buf.append(getPid());
return buf.toString();
}
/**
* Execute the command of the receiver process. Control will not return
* until the command list execution has finished.
*
* Default is to simply execute the command list of the receiver.
*
* @throws IOException
* if anything goes wrong during the execution
*/
protected void execute() throws IOException {
primitiveExecute(command);
}
protected String getControlDirectoryFileName() {
return CONTROL_DIR_PREFIX + id;
}
protected File getStdErrFile() {
return new File(directory, getStdErrFileName());
}
protected String getStdErrFileName() {
return inControlDirectory("std.err");
}
protected File getStdInFile() {
return new File(directory, getStdInFileName());
}
protected String getStdInFileName() {
return inControlDirectory("std.in");
}
protected File getStdOutFile() {
return new File(directory, getStdOutFileName());
}
protected String getStdOutFileName() {
return inControlDirectory("std.out");
}
protected String inControlDirectory(String fileName) {
return getControlDirectoryFileName() + File.separatorChar + fileName;
}
/**
* The actual execution process. Control will not return until the command
* list execution has finished.
*
* @param commands
* - the command list to execute
*
* @throws IOException
* - if anything goes wrong during the execution.
*/
protected void primitiveExecute(List<String> commands) throws IOException {
ProcessBuilder builder = new ProcessBuilder();
builder.directory(directory);
if (environment != null) {
builder.environment().putAll(environment);
}
builder.command(commands);
builder.redirectErrorStream(true); // combine OUT and ERR into one
// stream
Process p = builder.start();
final BufferedReader shellReader = new BufferedReader(
new InputStreamReader(
p.getInputStream()));
Runnable reader = new Runnable() {
@Override
public void run() {
String line;
try {
line = shellReader.readLine();
} catch (IOException e) {
if (!"Stream closed".equals(e.getMessage())
&& !e.getMessage().contains("Bad file descriptor")) {
log.log(Level.SEVERE, "Failed reading process output",
e);
}
return;
}
while (line != null) {
if (log.isLoggable(Level.FINE) && line != null) {
log.fine("[" + id + "] " + line);
}
try {
line = shellReader.readLine();
} catch (IOException e) {
if (!"Stream closed".equals(e.getMessage())) {
log.log(Level.SEVERE,
"Failed reading process output", e);
}
return;
}
}
}
};
Thread readerThread = new Thread(reader, "Process reader for: "
+ getCommand());
readerThread.setDaemon(true);
readerThread.start();
try {
p.waitFor();
} catch (InterruptedException e) {
return;
} finally {
readerThread.interrupt();
p.destroy();
}
}
}