package abbot.script;
import java.io.*;
import java.net.*;
import java.util.*;
import abbot.*;
import abbot.finder.AWTHierarchy;
import abbot.i18n.Strings;
import abbot.util.*;
import abbot.util.Properties;
/**
* A StepRunner that runs the step in a separate VM. Behavior should be
* indistinguishable from the base StepRunner.
*/
public class ForkedStepRunner extends StepRunner {
int LAUNCH_TIMEOUT = Properties.getProperty("abbot.runner.launch_delay",
60000, 0, 300000);
int TERMINATE_TIMEOUT =
Properties.getProperty("abbot.runner.terminate_delay",
30000, 0, 300000);
private static ServerSocket serverSocket = null;
private Process process = null;
private Socket connection = null;
/** When actually within the separate VM, this is what gets run. */
protected static class SlaveStepRunner extends StepRunner {
private Socket connection = null;
private Script script = null;
/** Notify the master when the application exits. */
protected SecurityManager createSecurityManager() {
return new ExitHandler() {
public void checkExit(int status) {
// handle application exit; send something back to
// the master if called from System.exit
String msg = Strings.get("runner.slave_premature_exit",
new Object[] {
new Integer(status) });
fireStepError(script, new Error(msg));
}
};
}
/** Translate the given event into something we can send back to the
* master.
*/
private void forwardEvent(StepEvent event) {
Step step = event.getStep();
final StringBuffer sb = new StringBuffer(encodeStep(script, step));
sb.append("\n");
sb.append(event.getType()); sb.append("\n");
sb.append(event.getID());
Throwable thr = event.getError();
if (thr != null) {
sb.append("\nMSG:"); sb.append(thr.getMessage());
sb.append("\nSTR:"); sb.append(thr.toString());
sb.append("\nTRC:");
StringWriter writer = new StringWriter();
thr.printStackTrace(new PrintWriter(writer));
sb.append(writer.toString());
}
try {
writeMessage(connection, sb.toString());
}
catch(IOException io) {
// nothing we can do
}
}
/** Handle running a script as a forked process. */
public void launchSlave(int port) {
// make connection back to originating port
try {
InetAddress local = InetAddress.getLocalHost();
connection = new Socket(local, port);
}
catch(Throwable thr) {
// Can't communicate so the only option is to quit
Log.warn(thr);
System.exit(1);
}
script = new Script(new AWTHierarchy());
try {
String dirName = readMessage(connection);
// Make sure the relative directory of this script is set
// properly.
script.setFile(new File(new File(dirName),
script.getFile().getName()));
String contents = readMessage(connection);
script.load(new StringReader(contents));
Log.debug("Successfully loaded script, dir=" + dirName);
// Make sure we only fork once!
script.setForked(false);
}
catch(IOException io) {
Log.warn(io);
System.exit(2);
}
catch(Throwable thr) {
Log.debug(thr);
StepEvent event = new StepEvent(script, StepEvent.STEP_ERROR,
0, thr);
forwardEvent(event);
}
// add listener to send messages back to the master
addStepListener(new StepListener() {
public void stateChanged(StepEvent ev) {
forwardEvent(ev);
}
});
// Run the script like we normally would. The listener handles
// all events and communication back to the launching process
try {
SlaveStepRunner.this.run(script);
}
catch(Throwable thr) {
// Listener catches all events and forwards them, so no one
// else is interested. just quit.
Log.debug(thr);
}
try {
connection.close();
}
catch(IOException io) {
// not much we can do
Log.warn(io);
}
// Return zero even on failure/error, since the script run itself
// worked, regardless of test results.
System.exit(0);
}
}
public ForkedStepRunner() {
this(null);
}
public ForkedStepRunner(StepRunner parent) {
super(parent != null ? parent.helper : null);
if (parent != null) {
setStopOnError(parent.getStopOnError());
setStopOnFailure(parent.getStopOnFailure());
setTerminateOnError(parent.getTerminateOnError());
}
}
Process fork(String vmargs, String[] cmdArgs) throws IOException {
String java = System.getProperty("java.home")
+ File.separator + "bin"
+ File.separator + "java";
ArrayList args = new ArrayList();
args.add(java);
args.add("-cp");
String cp = System.getProperty("java.class.path");
// Ensure the framework is included in the class path
String acp = System.getProperty("abbot.class.path");
if (acp != null) {
cp += System.getProperty("path.separator") + acp;
}
args.add(cp);
if (vmargs != null) {
StringTokenizer st = new StringTokenizer(vmargs);
while (st.hasMoreTokens()) {
args.add(st.nextToken());
}
}
args.addAll(Arrays.asList(cmdArgs));
if (Log.isClassDebugEnabled(getClass())) {
args.add("--debug");
args.add(getClass().getName());
}
cmdArgs = (String[])args.toArray(new String[args.size()]);
Process p = Runtime.getRuntime().exec(cmdArgs);
return p;
}
/** Launch a new process, using this class as the main class. */
Process fork(String vmargs) throws UnknownHostException, IOException {
if (serverSocket == null) {
serverSocket = new ServerSocket(0);
}
int localPort = serverSocket.getLocalPort();
String[] args = {
getClass().getName(), String.valueOf(localPort),
String.valueOf(getStopOnFailure()),
String.valueOf(getStopOnError()),
String.valueOf(getTerminateOnError())
};
Process p = fork(vmargs, args);
new ProcessOutputHandler(p) {
public void handleOutput(byte[] buf, int count) {
System.out.println("[out] " + new String(buf, 0, count));
}
public void handleError(byte[] buf, int count) {
System.err.println("[err] " + new String(buf, 0, count));
}
};
return p;
}
/** Running the step in a separate VM should be indistinguishable from
* running a regular script. When running as master, nothing actually
* runs locally. We just fork a subprocess and run the script in that,
* reporting back its progress as if it were running locally.
*/
public void runStep(Step step) throws Throwable {
Log.debug("run step " + step);
// Fire the start event prior to forking, then ignore the subsequent
// forked script start event when we get it.
fireStepStart(step);
process = null;
try {
Script script = (Script)step;
process = forkProcess(script.getVMArgs());
sendScript(script);
try {
trackScript(script);
try {
process.waitFor();
}
catch(InterruptedException e) {
}
try {
process.exitValue();
}
catch(IllegalThreadStateException its) {
try { Thread.sleep(TERMINATE_TIMEOUT); }
catch(InterruptedException ie) { }
// check again?
}
}
catch(IOException io) {
fireStepError(script, io);
if (getStopOnError())
throw io;
}
}
catch(AssertionFailedError afe) {
fireStepFailure(step, afe);
if (getStopOnFailure())
throw afe;
}
catch(Throwable thr) {
fireStepError(step, thr);
if (getStopOnError())
throw thr;
}
finally {
// Destroy it whether it's terminated or not.
if (process != null)
process.destroy();
}
fireStepEnd(step);
}
private Process forkProcess(String vmargs) throws Throwable {
try {
// fork new VM
// wait for connection
Process p = fork(vmargs);
serverSocket.setSoTimeout(LAUNCH_TIMEOUT);
connection = serverSocket.accept();
Log.debug("Got slave connection on " + connection);
return p;
}
catch(InterruptedIOException ie) {
Log.warn(ie);
throw new RuntimeException(Strings.get("runner.slave_timed_out"));
}
}
private void sendScript(Script script) throws IOException {
// send script data
StringWriter writer = new StringWriter();
script.save(writer);
writeMessage(connection, script.getDirectory().toString());
writeMessage(connection, writer.toString());
}
private void trackScript(Script script)
throws IOException, ForkedFailure, ForkedError {
StepEvent ev;
while (!stopped() && (ev = receiveEvent(script)) != null) {
Log.debug("Forked event received: " + ev);
// If it's the script start event, ignore it since we
// already sent one prior to launching the process
if (ev.getStep() == script
&& (StepEvent.STEP_START.equals(ev.getType())
|| StepEvent.STEP_END.equals(ev.getType()))) {
continue;
}
Log.debug("Replaying forked event locally " + ev);
Throwable err = ev.getError();
if (err != null) {
setError(ev.getStep(), err);
fireStepEvent(ev);
if (err instanceof AssertionFailedError) {
if (getStopOnFailure())
throw (ForkedFailure)err;
}
else {
if (getStopOnError())
throw (ForkedError)err;
}
}
else {
fireStepEvent(ev);
}
}
}
static Step decodeStep(Sequence root, String code) {
if (code.equals("-1"))
return root;
int comma = code.indexOf(",");
if (comma == -1) {
// Let number format exceptions propagate up, since it's a fatal
// script error.
int index = Integer.parseInt(code);
return root.getStep(index);
}
String ind = code.substring(0, comma);
code = code.substring(comma + 1);
return decodeStep((Sequence)root.getStep(Integer.parseInt(ind)), code);
}
/** Encode the given step into a set of indices. */
static String encodeStep(Sequence root, Step step) {
if (root.equals(step))
return "-1";
synchronized(root.steps()) {
int index = root.indexOf(step);
if (index != -1)
return String.valueOf(index);
index = 0;
Iterator iter = root.steps().iterator();
while (iter.hasNext()) {
Step seq = (Step)iter.next();
if (seq instanceof Sequence) {
String encoding = encodeStep((Sequence)seq, step);
if (encoding != null) {
return index + "," + encoding;
}
}
++index;
}
return null;
}
}
/** Receive a serialized event on the connection and convert it back into
* a real event, setting the local representation of the given step's
* exception/error if necessary.
*/
private StepEvent receiveEvent(Script script) throws IOException {
String buf = readMessage(connection);
if (buf == null) {
Log.debug("End of stream");
return null; // end of stream
}
StringTokenizer st = new StringTokenizer(buf, "\n");
String code = st.nextToken();
String type = st.nextToken();
String id = st.nextToken();
Step step = decodeStep(script, code);
Throwable thr = null;
if (st.hasMoreTokens()) {
String msg = st.nextToken();
String string;
String trace;
msg = msg.substring(4);
String next = st.nextToken();
while (!next.startsWith("STR:")) {
msg += next;
next = st.nextToken();
}
string = next.substring(4);
next = st.nextToken();
while (!next.startsWith("TRC:")) {
string += next;
next = st.nextToken();
}
trace = next.substring(4);
while (st.hasMoreTokens()) {
trace = trace + "\n" + st.nextToken();
}
if (type.equals(StepEvent.STEP_FAILURE)) {
Log.debug("Creating local forked step failure");
thr = new ForkedFailure(msg, string, trace);
}
else {
Log.debug("Creating local forked step error");
thr = new ForkedError(msg, string, trace);
}
}
StepEvent event =
new StepEvent(step, type, Integer.parseInt(id), thr);
return event;
}
private static void writeMessage(Socket connection, String msg)
throws IOException {
OutputStream os = connection.getOutputStream();
byte[] buf = msg.getBytes();
int len = buf.length;
for (int i=0;i < 4;i++) {
byte val = (byte)(len >> 24);
os.write(val);
len <<= 8;
}
os.write(buf, 0, buf.length);
os.flush();
}
private static String readMessage(Socket connection) throws IOException {
InputStream is = connection.getInputStream();
// FIXME probably want a socket timeout, in case the slave script
// hangs
int len = 0;
for (int i=0;i < 4;i++) {
int data = is.read();
if (data == -1) {
return null;
}
len = (len << 8) | data;
}
byte[] buf = new byte[len];
int offset = 0;
while (offset < len) {
int count = is.read(buf, offset, buf.length - offset);
if (count == -1) {
return null;
}
offset += count;
}
String msg = new String(buf, 0, len);
return msg;
}
/** An exception that for all purposes looks like another exception. */
class ForkedFailure extends AssertionFailedError {
private String msg;
private String str;
private String trace;
public ForkedFailure(String msg, String str, String trace) {
this.msg = msg + " (forked)";
this.str = str + " (forked)";
this.trace = trace + " (forked)";
}
public String getMessage() { return msg; }
public String toString() { return str; }
public void printStackTrace(PrintWriter writer) {
synchronized(writer) {
writer.print(trace);
}
}
public void printStackTrace(PrintStream s) {
synchronized(s) {
s.print(trace);
}
}
public void printStackTrace() {
printStackTrace(System.err);
}
}
/** An exception that for all purposes looks like another exception. */
class ForkedError extends RuntimeException {
private String msg;
private String str;
private String trace;
public ForkedError(String msg, String str, String trace) {
this.msg = msg + " (forked)";
this.str = str + " (forked)";
this.trace = trace + " (forked)";
}
public String getMessage() { return msg; }
public String toString() { return str; }
public void printStackTrace(PrintWriter writer) {
synchronized(writer) {
writer.print(trace);
}
}
public void printStackTrace(PrintStream s) {
synchronized(s) {
s.print(trace);
}
}
public void printStackTrace() {
printStackTrace(System.err);
}
}
/** Provide means to control execution and feedback of a script in a
separate process.
*/
public static void main(String[] args) {
args = Log.init(args);
try {
final int port = Integer.parseInt(args[0]);
final SlaveStepRunner runner = new SlaveStepRunner();
runner.setStopOnFailure("true".equals(args[1]));
runner.setStopOnError("true".equals(args[2]));
runner.setTerminateOnError("true".equals(args[3]));
new Thread(new Runnable() {
public void run() {
runner.launchSlave(port);
}
}, "Forked script").start();
}
catch(Throwable e) {
System.err.println("usage: abbot.script.ForkedStepRunner <port>");
System.exit(1);
}
}
}