001: package abbot.script;
002:
003: import java.io.*;
004: import java.net.*;
005: import java.util.*;
006:
007: import abbot.*;
008: import abbot.finder.AWTHierarchy;
009: import abbot.i18n.Strings;
010: import abbot.util.*;
011: import abbot.util.Properties;
012:
013: /**
014: * A StepRunner that runs the step in a separate VM. Behavior should be
015: * indistinguishable from the base StepRunner.
016: */
017: public class ForkedStepRunner extends StepRunner {
018:
019: int LAUNCH_TIMEOUT = Properties.getProperty(
020: "abbot.runner.launch_delay", 60000, 0, 300000);
021: int TERMINATE_TIMEOUT = Properties.getProperty(
022: "abbot.runner.terminate_delay", 30000, 0, 300000);
023:
024: private static ServerSocket serverSocket = null;
025: private Process process = null;
026: private Socket connection = null;
027:
028: /** When actually within the separate VM, this is what gets run. */
029: protected static class SlaveStepRunner extends StepRunner {
030: private Socket connection = null;
031: private Script script = null;
032:
033: /** Notify the master when the application exits. */
034: protected SecurityManager createSecurityManager() {
035: return new ExitHandler() {
036: public void checkExit(int status) {
037: // handle application exit; send something back to
038: // the master if called from System.exit
039: String msg = Strings.get(
040: "runner.slave_premature_exit",
041: new Object[] { new Integer(status) });
042: fireStepError(script, new Error(msg));
043: }
044: };
045: }
046:
047: /** Translate the given event into something we can send back to the
048: * master.
049: */
050: private void forwardEvent(StepEvent event) {
051: Step step = event.getStep();
052: final StringBuffer sb = new StringBuffer(encodeStep(script,
053: step));
054: sb.append("\n");
055: sb.append(event.getType());
056: sb.append("\n");
057: sb.append(event.getID());
058: Throwable thr = event.getError();
059: if (thr != null) {
060: sb.append("\nMSG:");
061: sb.append(thr.getMessage());
062: sb.append("\nSTR:");
063: sb.append(thr.toString());
064: sb.append("\nTRC:");
065: StringWriter writer = new StringWriter();
066: thr.printStackTrace(new PrintWriter(writer));
067: sb.append(writer.toString());
068: }
069: try {
070: writeMessage(connection, sb.toString());
071: } catch (IOException io) {
072: // nothing we can do
073: }
074: }
075:
076: /** Handle running a script as a forked process. */
077: public void launchSlave(int port) {
078: // make connection back to originating port
079: try {
080: InetAddress local = InetAddress.getLocalHost();
081: connection = new Socket(local, port);
082: } catch (Throwable thr) {
083: // Can't communicate so the only option is to quit
084: Log.warn(thr);
085: System.exit(1);
086: }
087: script = new Script(new AWTHierarchy());
088: try {
089: String dirName = readMessage(connection);
090: // Make sure the relative directory of this script is set
091: // properly.
092: script.setFile(new File(new File(dirName), script
093: .getFile().getName()));
094: String contents = readMessage(connection);
095: script.load(new StringReader(contents));
096: Log.debug("Successfully loaded script, dir=" + dirName);
097: // Make sure we only fork once!
098: script.setForked(false);
099: } catch (IOException io) {
100: Log.warn(io);
101: System.exit(2);
102: } catch (Throwable thr) {
103: Log.debug(thr);
104: StepEvent event = new StepEvent(script,
105: StepEvent.STEP_ERROR, 0, thr);
106: forwardEvent(event);
107: }
108:
109: // add listener to send messages back to the master
110: addStepListener(new StepListener() {
111: public void stateChanged(StepEvent ev) {
112: forwardEvent(ev);
113: }
114: });
115:
116: // Run the script like we normally would. The listener handles
117: // all events and communication back to the launching process
118: try {
119: SlaveStepRunner.this .run(script);
120: } catch (Throwable thr) {
121: // Listener catches all events and forwards them, so no one
122: // else is interested. just quit.
123: Log.debug(thr);
124: }
125:
126: try {
127: connection.close();
128: } catch (IOException io) {
129: // not much we can do
130: Log.warn(io);
131: }
132: // Return zero even on failure/error, since the script run itself
133: // worked, regardless of test results.
134: System.exit(0);
135: }
136: }
137:
138: public ForkedStepRunner() {
139: this (null);
140: }
141:
142: public ForkedStepRunner(StepRunner parent) {
143: super (parent != null ? parent.helper : null);
144: if (parent != null) {
145: setStopOnError(parent.getStopOnError());
146: setStopOnFailure(parent.getStopOnFailure());
147: setTerminateOnError(parent.getTerminateOnError());
148: }
149: }
150:
151: Process fork(String vmargs, String[] cmdArgs) throws IOException {
152: String java = System.getProperty("java.home") + File.separator
153: + "bin" + File.separator + "java";
154: ArrayList args = new ArrayList();
155: args.add(java);
156: args.add("-cp");
157: String cp = System.getProperty("java.class.path");
158: // Ensure the framework is included in the class path
159: String acp = System.getProperty("abbot.class.path");
160: if (acp != null) {
161: cp += System.getProperty("path.separator") + acp;
162: }
163: args.add(cp);
164: if (vmargs != null) {
165: StringTokenizer st = new StringTokenizer(vmargs);
166: while (st.hasMoreTokens()) {
167: args.add(st.nextToken());
168: }
169: }
170: args.addAll(Arrays.asList(cmdArgs));
171: if (Log.isClassDebugEnabled(getClass())) {
172: args.add("--debug");
173: args.add(getClass().getName());
174: }
175: cmdArgs = (String[]) args.toArray(new String[args.size()]);
176: Process p = Runtime.getRuntime().exec(cmdArgs);
177: return p;
178: }
179:
180: /** Launch a new process, using this class as the main class. */
181: Process fork(String vmargs) throws UnknownHostException,
182: IOException {
183: if (serverSocket == null) {
184: serverSocket = new ServerSocket(0);
185: }
186: int localPort = serverSocket.getLocalPort();
187: String[] args = { getClass().getName(),
188: String.valueOf(localPort),
189: String.valueOf(getStopOnFailure()),
190: String.valueOf(getStopOnError()),
191: String.valueOf(getTerminateOnError()) };
192: Process p = fork(vmargs, args);
193: new ProcessOutputHandler(p) {
194: public void handleOutput(byte[] buf, int count) {
195: System.out
196: .println("[out] " + new String(buf, 0, count));
197: }
198:
199: public void handleError(byte[] buf, int count) {
200: System.err
201: .println("[err] " + new String(buf, 0, count));
202: }
203: };
204: return p;
205: }
206:
207: /** Running the step in a separate VM should be indistinguishable from
208: * running a regular script. When running as master, nothing actually
209: * runs locally. We just fork a subprocess and run the script in that,
210: * reporting back its progress as if it were running locally.
211: */
212: public void runStep(Step step) throws Throwable {
213: Log.debug("run step " + step);
214: // Fire the start event prior to forking, then ignore the subsequent
215: // forked script start event when we get it.
216: fireStepStart(step);
217: process = null;
218: try {
219: Script script = (Script) step;
220: process = forkProcess(script.getVMArgs());
221: sendScript(script);
222: try {
223: trackScript(script);
224: try {
225: process.waitFor();
226: } catch (InterruptedException e) {
227: }
228: try {
229: process.exitValue();
230: } catch (IllegalThreadStateException its) {
231: try {
232: Thread.sleep(TERMINATE_TIMEOUT);
233: } catch (InterruptedException ie) {
234: }
235: // check again?
236: }
237: } catch (IOException io) {
238: fireStepError(script, io);
239: if (getStopOnError())
240: throw io;
241: }
242: } catch (junit.framework.AssertionFailedError afe) {
243: fireStepFailure(step, afe);
244: if (getStopOnFailure())
245: throw afe;
246: } catch (Throwable thr) {
247: fireStepError(step, thr);
248: if (getStopOnError())
249: throw thr;
250: } finally {
251: // Destroy it whether it's terminated or not.
252: if (process != null)
253: process.destroy();
254: }
255: fireStepEnd(step);
256: }
257:
258: private Process forkProcess(String vmargs) throws Throwable {
259: try {
260: // fork new VM
261: // wait for connection
262: Process p = fork(vmargs);
263: serverSocket.setSoTimeout(LAUNCH_TIMEOUT);
264: connection = serverSocket.accept();
265: Log.debug("Got slave connection on " + connection);
266: return p;
267: } catch (InterruptedIOException ie) {
268: Log.warn(ie);
269: throw new RuntimeException(Strings
270: .get("runner.slave_timed_out"));
271: }
272: }
273:
274: private void sendScript(Script script) throws IOException {
275: // send script data
276: StringWriter writer = new StringWriter();
277: script.save(writer);
278: writeMessage(connection, script.getDirectory().toString());
279: writeMessage(connection, writer.toString());
280: }
281:
282: private void trackScript(Script script) throws IOException,
283: ForkedFailure, ForkedError {
284: StepEvent ev;
285: while (!stopped() && (ev = receiveEvent(script)) != null) {
286: Log.debug("Forked event received: " + ev);
287: // If it's the script start event, ignore it since we
288: // already sent one prior to launching the process
289: if (ev.getStep() == script
290: && (StepEvent.STEP_START.equals(ev.getType()) || StepEvent.STEP_END
291: .equals(ev.getType()))) {
292: continue;
293: }
294:
295: Log.debug("Replaying forked event locally " + ev);
296: Throwable err = ev.getError();
297: if (err != null) {
298: setError(ev.getStep(), err);
299: fireStepEvent(ev);
300: if (err instanceof junit.framework.AssertionFailedError) {
301: if (getStopOnFailure())
302: throw (ForkedFailure) err;
303: } else {
304: if (getStopOnError())
305: throw (ForkedError) err;
306: }
307: } else {
308: fireStepEvent(ev);
309: }
310: }
311: }
312:
313: static Step decodeStep(Sequence root, String code) {
314: if (code.equals("-1"))
315: return root;
316: int comma = code.indexOf(",");
317: if (comma == -1) {
318: // Let number format exceptions propagate up, since it's a fatal
319: // script error.
320: int index = Integer.parseInt(code);
321: return root.getStep(index);
322: }
323: String ind = code.substring(0, comma);
324: code = code.substring(comma + 1);
325: return decodeStep((Sequence) root
326: .getStep(Integer.parseInt(ind)), code);
327: }
328:
329: /** Encode the given step into a set of indices. */
330: static String encodeStep(Sequence root, Step step) {
331: if (root.equals(step))
332: return "-1";
333: synchronized (root.steps()) {
334: int index = root.indexOf(step);
335: if (index != -1)
336: return String.valueOf(index);
337: index = 0;
338: Iterator iter = root.steps().iterator();
339: while (iter.hasNext()) {
340: Step seq = (Step) iter.next();
341: if (seq instanceof Sequence) {
342: String encoding = encodeStep((Sequence) seq, step);
343: if (encoding != null) {
344: return index + "," + encoding;
345: }
346: }
347: ++index;
348: }
349: return null;
350: }
351: }
352:
353: /** Receive a serialized event on the connection and convert it back into
354: * a real event, setting the local representation of the given step's
355: * exception/error if necessary.
356: */
357: private StepEvent receiveEvent(Script script) throws IOException {
358: String buf = readMessage(connection);
359: if (buf == null) {
360: Log.debug("End of stream");
361: return null; // end of stream
362: }
363: StringTokenizer st = new StringTokenizer(buf, "\n");
364: String code = st.nextToken();
365: String type = st.nextToken();
366: String id = st.nextToken();
367: Step step = decodeStep(script, code);
368: Throwable thr = null;
369: if (st.hasMoreTokens()) {
370: String msg = st.nextToken();
371: String string;
372: String trace;
373: msg = msg.substring(4);
374: String next = st.nextToken();
375: while (!next.startsWith("STR:")) {
376: msg += next;
377: next = st.nextToken();
378: }
379: string = next.substring(4);
380: next = st.nextToken();
381: while (!next.startsWith("TRC:")) {
382: string += next;
383: next = st.nextToken();
384: }
385: trace = next.substring(4);
386: while (st.hasMoreTokens()) {
387: trace = trace + "\n" + st.nextToken();
388: }
389:
390: if (type.equals(StepEvent.STEP_FAILURE)) {
391: Log.debug("Creating local forked step failure");
392: thr = new ForkedFailure(msg, string, trace);
393: } else {
394: Log.debug("Creating local forked step error");
395: thr = new ForkedError(msg, string, trace);
396: }
397: }
398: StepEvent event = new StepEvent(step, type, Integer
399: .parseInt(id), thr);
400: return event;
401: }
402:
403: private static void writeMessage(Socket connection, String msg)
404: throws IOException {
405: OutputStream os = connection.getOutputStream();
406: byte[] buf = msg.getBytes();
407: int len = buf.length;
408: for (int i = 0; i < 4; i++) {
409: byte val = (byte) (len >> 24);
410: os.write(val);
411: len <<= 8;
412: }
413: os.write(buf, 0, buf.length);
414: os.flush();
415: }
416:
417: private static String readMessage(Socket connection)
418: throws IOException {
419: InputStream is = connection.getInputStream();
420: // FIXME probably want a socket timeout, in case the slave script
421: // hangs
422: int len = 0;
423: for (int i = 0; i < 4; i++) {
424: int data = is.read();
425: if (data == -1) {
426: return null;
427: }
428: len = (len << 8) | data;
429: }
430: byte[] buf = new byte[len];
431: int offset = 0;
432: while (offset < len) {
433: int count = is.read(buf, offset, buf.length - offset);
434: if (count == -1) {
435: return null;
436: }
437: offset += count;
438: }
439: String msg = new String(buf, 0, len);
440: return msg;
441: }
442:
443: /** An exception that looks almost exactly like
444: some other exception, without actually having access
445: to the instance of the original exception.
446: */
447: class ForkedFailure extends AssertionFailedError {
448: private String msg;
449: private String str;
450: private String trace;
451:
452: public ForkedFailure(String msg, String str, String trace) {
453: this .msg = msg + " (forked)";
454: this .str = str + " (forked)";
455: this .trace = trace + " (forked)";
456: }
457:
458: public String getMessage() {
459: return msg;
460: }
461:
462: public String toString() {
463: return str;
464: }
465:
466: public void printStackTrace(PrintWriter writer) {
467: synchronized (writer) {
468: writer.print(trace);
469: }
470: }
471:
472: public void printStackTrace(PrintStream s) {
473: synchronized (s) {
474: s.print(trace);
475: }
476: }
477:
478: public void printStackTrace() {
479: printStackTrace(System.err);
480: }
481: }
482:
483: /** An exception that for all purposes looks like another exception. */
484: class ForkedError extends RuntimeException {
485: private String msg;
486: private String str;
487: private String trace;
488:
489: public ForkedError(String msg, String str, String trace) {
490: this .msg = msg + " (forked)";
491: this .str = str + " (forked)";
492: this .trace = trace + " (forked)";
493: }
494:
495: public String getMessage() {
496: return msg;
497: }
498:
499: public String toString() {
500: return str;
501: }
502:
503: public void printStackTrace(PrintWriter writer) {
504: synchronized (writer) {
505: writer.print(trace);
506: }
507: }
508:
509: public void printStackTrace(PrintStream s) {
510: synchronized (s) {
511: s.print(trace);
512: }
513: }
514:
515: public void printStackTrace() {
516: printStackTrace(System.err);
517: }
518: }
519:
520: /** Provide means to control execution and feedback of a script in a
521: separate process.
522: */
523: public static void main(String[] args) {
524: args = Log.init(args);
525: try {
526: final int port = Integer.parseInt(args[0]);
527: final SlaveStepRunner runner = new SlaveStepRunner();
528: runner.setStopOnFailure("true".equals(args[1]));
529: runner.setStopOnError("true".equals(args[2]));
530: runner.setTerminateOnError("true".equals(args[3]));
531: new Thread(new Runnable() {
532: public void run() {
533: runner.launchSlave(port);
534: }
535: }, "Forked script").start();
536: } catch (Throwable e) {
537: System.err
538: .println("usage: abbot.script.ForkedStepRunner <port>");
539: System.exit(1);
540: }
541: }
542: }
|