/** (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.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.hellblazer.process.CannotStopProcessException;
/**
* @author Hal Hildebrand
*
*/
public class UnixProcess extends AbstractManagedProcess {
private static String[] activeStates = new String[] { "U", "I",
"R", "S" };
private static final Logger log = Logger.getLogger(UnixProcess.class.getCanonicalName());
private static final long serialVersionUID = 1L;
private static final String[] VALID_STATES = new String[] { "D", "R",
"S", "T", "Z", "U", "I", "L", "W" };
protected Integer exitValue;
protected Integer pid;
protected int wrapperPid;
public UnixProcess() {
super();
}
public UnixProcess(UUID id) {
super(id);
}
@Override
public void acquireFromHome(File homeDirectory) {
setDirectory(homeDirectory);
try {
wrapperPid = readPid(getWrapperPidFile());
pid = readPid(getPidFile());
} catch (IllegalStateException e) {
// process not started
}
}
@Override
public Integer getExitValue() {
if (pid == null || terminated) {
return exitValue;
}
File exitValueFile = getExitValueFile();
if (log.isLoggable(Level.FINE)) {
log.fine("looking for exit value file: "
+ exitValueFile.getAbsolutePath());
}
FileInputStream is = null;
try {
is = new FileInputStream(exitValueFile);
} catch (FileNotFoundException e1) {
if (log.isLoggable(Level.FINE)) {
log.fine("Process has not terminated");
}
return null;
}
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String line = null;
try {
line = reader.readLine();
exitValue = Integer.parseInt(line);
} catch (NumberFormatException e) {
throw new IllegalStateException("Unable to parse exit value: {"
+ line + "} of process [" + id
+ "]");
} catch (IOException e) {
throw new IllegalStateException(
"Unable to retrieve exit value of process ["
+ id + "]");
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
log.fine(String.format("error closing", e));
}
}
}
try {
reader.close();
} catch (IOException e) {
// ignore
}
return exitValue;
}
@Override
public Integer getPid() {
return pid;
}
@Override
public boolean isActive() {
return isActive(pid);
}
public boolean isActive(Integer thePid) {
if (thePid == null) {
if (log.isLoggable(Level.FINE)) {
log.fine("inactive for " + this);
}
return false;
}
String status = getProcessStatus(thePid);
if (status == null) {
if (log.isLoggable(Level.FINE)) {
log.fine("inactive, no status for " + this);
}
return false;
}
for (String active : getActiveStates()) {
if (status.startsWith(active)) {
return true;
}
}
if (log.isLoggable(Level.FINE)) {
log.fine("inactive, status is " + status + " for " + this);
}
return false;
}
@Override
public synchronized void start() throws IOException {
if (isActive()) {
return;
}
super.start();
wrapperPid = readPid(getWrapperPidFile());
pid = readPid(getPidFile());
if (log.isLoggable(Level.FINE)) {
log.fine("started [" + id + "] pid=" + pid);
}
}
/*
* (non-Javadoc)
*
* @see com.hellblazer.process.ManagedProcess#stop()
*/
@Override
public synchronized void stop(int waitForSeconds)
throws CannotStopProcessException {
if (isDead()) {
return;
}
if (log.isLoggable(Level.FINE)) {
log.fine("stopping: " + this);
}
// Be nice about it.
kill();
long target = System.currentTimeMillis() + waitForSeconds * 1000;
while (System.currentTimeMillis() < target && !isDead()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
return;
}
}
if (!isDead()) {
log.info("Cannot kill: PID=" + pid + " " + command
+ " resorting to kill -9");
// Okay, then. Terminate with extreme prejudice
kill(9);
}
if (!isDead()) {
throw new CannotStopProcessException("Cannot stop process. PID="
+ pid + " " + command);
}
}
@Override
public int waitFor() throws InterruptedException {
if (terminated) {
return getExitValue();
}
waitFor(wrapperPid);
return getExitValue();
}
private boolean invalidStatus(String line) {
if (line.startsWith("STAT")) {
return true;
}
for (String state : VALID_STATES) {
if (line.startsWith(state)) {
return false;
}
}
return true;
}
/**
* @param reader
* @param line
*/
private void logStatusErrorOutput(BufferedReader reader, String line) {
if (log.isLoggable(Level.FINE)) {
StringBuffer out = new StringBuffer(60);
String errLine = line;
while (errLine != null) {
out.append(errLine);
out.append("\n");
try {
errLine = reader.readLine();
} catch (IOException e) {
out.append("*** Unable to retrieve further output: "
+ e.getMessage());
break;
}
}
log.fine("ps error output: \n" + out.toString());
}
}
@Override
protected void execute() throws IOException {
writeScript();
List<String> scriptCmnds = new ArrayList<String>();
scriptCmnds.add("/bin/sh");
scriptCmnds.add(getScriptFile().getAbsolutePath());
primitiveExecute(scriptCmnds);
}
/**
* @return the array of strings which represent active process state
*/
protected String[] getActiveStates() {
return activeStates;
}
protected File getExitValueFile() {
return new File(directory, getExitValueFileName());
}
protected String getExitValueFileName() {
return inControlDirectory("exit.value");
}
protected File getPidFile() {
return new File(directory, getPidFileName());
}
protected String getPidFileName() {
return inControlDirectory("pid");
}
/**
* @return
*/
protected String getProcessStatus(Integer thePid) {
if ((thePid == null) || (thePid == -1)) {
return null;
}
ProcessBuilder ps = new ProcessBuilder();
ps.command(new String[] { "ps", "-o", "state", "-p",
String.valueOf(thePid) });
ps.redirectErrorStream(true);
if (log.isLoggable(Level.FINE)) {
log.fine("requesting process status: " + ps.command());
}
Process psProc;
try {
psProc = ps.start();
} catch (IOException e) {
throw new IllegalStateException("Unable to start ps -o state -p "
+ thePid, e);
}
BufferedReader reader = new BufferedReader(
new InputStreamReader(
psProc.getInputStream()));
int status;
try {
status = psProc.waitFor();
} catch (InterruptedException e) {
return "";
}
String line;
try {
line = reader.readLine();
while (line != null && invalidStatus(line)) {
line = reader.readLine();
}
} catch (IOException e) {
throw new IllegalStateException("Unable to parse status for pid="
+ thePid, e);
}
if (status == 1) {
return null; // process does not exist
}
if (status != 0) {
log.severe("Retrieval of status for pid=" + thePid
+ " failed with status code " + status);
logStatusErrorOutput(reader, line);
throw new IllegalStateException("Retrieval of status for pid="
+ thePid
+ " failed with status code "
+ status);
}
if (line == null) {
return null; // process does not exist
}
if ("STAT".equals(line)) {
log.fine("ignoring 'STAT' header");
try {
line = reader.readLine();
} catch (IOException e) {
throw new IllegalStateException(
"Unable to parse status for pid="
+ thePid, e);
}
}
if (log.isLoggable(Level.FINE)) {
log.fine("pid=" + pid + " status: " + line);
}
return line;
}
protected File getScriptFile() {
return new File(directory, getScriptFileName());
}
protected String getScriptFileName() {
return inControlDirectory("run.sh");
}
protected File getWrapperPidFile() {
return new File(directory, getWrapperPidFileName());
}
protected String getWrapperPidFileName() {
return inControlDirectory("wrapper.pid");
}
/**
* @return true if the process is well and truly dead
*/
protected boolean isDead() {
if (terminated || getProcessStatus(pid) == null) {
terminated = true;
}
return terminated;
}
protected boolean isProcessDead(int thePid) {
String line = getProcessStatus(thePid);
if (line == null) {
return true;
}
return false;
}
protected void kill() {
if (pid == null) {
return;
}
ProcessBuilder kill = new ProcessBuilder();
kill.command(new String[] { "kill", String.valueOf(pid) });
kill.redirectErrorStream(true);
Process killProc;
try {
killProc = kill.start();
} catch (IOException e) {
throw new IllegalStateException("Unable to start kill pid=" + pid,
e);
}
try {
killProc.waitFor();
} catch (InterruptedException e) {
return;
}
}
protected void kill(int signal) {
if (pid == null) {
return;
}
ProcessBuilder kill = new ProcessBuilder();
kill.command(new String[] { "kill", "-" + signal, String.valueOf(pid) });
kill.redirectErrorStream(true);
Process killProc;
try {
killProc = kill.start();
} catch (IOException e) {
throw new IllegalStateException("Unable to start kill -" + signal
+ " -p" + pid, e);
}
try {
killProc.waitFor();
} catch (InterruptedException e) {
return;
}
}
protected int readPid(File pidFile) {
for (int i = 0; i < 1000; i++) {
if (pidFile.exists() && pidFile.length() > 0) {
break;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
return -1;
}
}
if (!pidFile.exists()) {
throw new IllegalStateException("Required PID file is missing! <"
+ pidFile + ">");
}
try {
BufferedReader pidStream = new BufferedReader(
new InputStreamReader(
new FileInputStream(
pidFile)));
String pidNum = pidStream.readLine();
if (pidNum == null) {
try {
pidStream.close();
} catch (IOException e) {
log.fine(String.format("error closing stream", e));
}
throw new IllegalStateException("pid is empty <" + pidFile
+ ">");
}
int thePid = Integer.parseInt(pidNum);
pidStream.close();
return thePid;
} catch (IOException e) {
throw new IllegalStateException("Unable to read PID file <"
+ pidFile + ">");
}
}
protected void waitFor(int thePid) throws InterruptedException {
while (isActive(thePid)) {
Thread.sleep(10);
}
for (int i = 0; i < 10; i++) {
if (!getExitValueFile().exists()) {
Thread.sleep(10);
}
}
}
/**
* Output the scripts which will actually run the process in the background,
* capturing outptut streams, PID and exit value.
*
* Script is of the form:
*
* #!/bin/sh exec 1> .control-905eda8c-e0cf-40c7-9169-fbe16c601ae7/std.out
* exec 2> .control-905eda8c-e0cf-40c7-9169-fbe16c601ae7/std.err (nohup
* {quoted command} < {ctrl-dir}/std.in & x=$!; echo $x > {ctrl-dir}/pid;
* wait $x; echo $? > {ctrl-dir}/exit.value)& echo $! >
* {ctrl-dir}/wrapper.pid
*
*/
protected void writeScript() throws IOException {
PrintWriter script = new PrintWriter(
new OutputStreamWriter(
new FileOutputStream(
getScriptFile())));
script.println("#!/bin/sh");
script.append("exec 1> ");
script.append(getStdOutFileName());
script.println();
script.append("exec 2> ");
script.append(getStdErrFileName());
script.println();
script.append('(');
script.append("nohup ");
for (String part : command) {
script.append('"').append(part).append('"');
script.append(' ');
}
script.append(" < ");
script.append(getStdInFileName());
script.append(" & x=$!; echo $x > ");
script.append(getPidFileName());
script.append("; wait $x; echo $? > ");
script.append(getExitValueFileName());
script.println(")&");
script.append("echo $! > ");
script.println(getWrapperPidFileName());
script.flush();
script.close();
}
}