/******************************************************************************* * Copyright (c) 2012 Google, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Google, Inc. - initial API and implementation *******************************************************************************/ package com.windowtester.internal.runtime.monitor; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.util.Iterator; import java.util.TreeSet; import org.eclipse.swt.widgets.Display; import com.windowtester.internal.debug.IRuntimePluginTraceOptions; import com.windowtester.internal.debug.Logger; import com.windowtester.internal.debug.ThreadUtil; import com.windowtester.internal.debug.Tracer; import com.windowtester.runtime.IUIContext; import com.windowtester.runtime.condition.IConditionMonitor; import com.windowtester.runtime.monitor.IUIThreadMonitor; import com.windowtester.runtime.monitor.IUIThreadMonitorListener; import com.windowtester.runtime.util.ScreenCapture; /** * Abstract superclass providing common behavior for objects that monitor the UI Thread * and notifies listeners if the UI thread is either hung or idle for an extended period * of time. Typically this is accomplished by launching a background thread (minimum * priority) that checks to see if the UI is responsive and processing input. If the UI * becomes unresponsive or idle for a period longer than expected, then the associated * {@link com.windowtester.runtime.monitor.IUIThreadMonitorListener} (see * {@link com.windowtester.runtime.monitor.IUIThreadMonitor#setListener(com.windowtester.runtime.monitor.IUIThreadMonitorListener)}) * is notified. */ public abstract class UIThreadMonitorCommon implements IUIThreadMonitor { // Tracing Constants private static final int TRACE_UNINITIALIZED = 0; private static final int TRACE_OFF = 1; private static final int TRACE_ON = 2; private static final int TRACE_CONSOLE = 3; private static int _traceMode = TRACE_UNINITIALIZED; /** * The user interface context used by the receiver to handle conditions when the UI * thread has been idle too long in an attempt to get the test running again. */ private final IUIContext _uiContext; /** * The object to synchronize against when accessing the {@link #_listener}, * {@link #_defaultExpectedDelay}, {@link #_uiThreadResponsive}, * {@link #_uiEventProcessed}, {@link #_testExecutesUntil} and {@link #_uiBusyUntil} * fields. */ protected final Object _lock = new Object(); /** * The listener associated with the receiver that will be notified if the UI is no * longer responsive and processing input. Synchronize against {@link #_lock} when * accessing this field. */ private IUIThreadMonitorListener _listener; /** * Default expected delay... 2 minutes unless overridden by a call to * {@link #setDefaultExpectedDelay(long)}. Synchronize against {@link #_lock} when * accessing this field. */ private long _defaultExpectedDelay = 120000; /** * Flag indicating that the UI is responsive. Synchronize against {@link #_lock} when * accessing this field. See {@link #_threadResponsiveRunnable} and * {@link #checkUI(Display, long)}. */ protected boolean _uiThreadResponsive; /** * Flag indicating that the UI is processing events. Synchronize against * {@link #_lock} when accessing this field. See {@link #_swtEventListener} and * {@link #checkUI(Display, long)}. */ private boolean _uiEventProcessed; /** * The system time before which the UI thread should have processed a new event. This * is set as needed during test execution by calling {@link #expectDelay(long)}. * Synchronize against {@link #_lock} when accessing this field. */ private long _uiBusyUntil; /** * Stack traces at time of UI inactivity generated by {@link #fireUITimeout(boolean)} * and consumed by {@link #writeResults(PrintWriter, boolean)} */ private String stackTraces; /** * Screen capture at time of UI inactivity generated by * {@link #fireUITimeout(boolean)} and consumed by * {@link #writeResults(PrintWriter, boolean)} */ private File screenCapture; // ////////////////////////////////////////////////////////////////////////// // // Constructor // // ////////////////////////////////////////////////////////////////////////// /** * Construct a new instance to monitor the health of the user interface thread. * * @param uiContext the user interface context (not <code>null</code>) */ public UIThreadMonitorCommon(IUIContext uiContext) { if (uiContext == null) throw new IllegalArgumentException(); _uiContext = uiContext; } // ////////////////////////////////////////////////////////////////////////// // // Listener // // ////////////////////////////////////////////////////////////////////////// /** * Answer the current thread monitor listener. * * @return the listener or <code>null</code> if none. */ protected IUIThreadMonitorListener getListener() { synchronized (_lock) { return _listener; } } /* * (non-Javadoc) * * @see com.windowtester.runtime2.monitor.IUIThreadMonitor#setListener(com.windowtester.runtime2.monitor.IUIThreadMonitorListener) */ public void setListener(IUIThreadMonitorListener newListener) { com.windowtester.runtime.monitor.IUIThreadMonitorListener oldListener; synchronized (_lock) { oldListener = _listener; _listener = newListener; } trace("setListener ", newListener); if (newListener != null) { if (oldListener == null) { addEventListeners(); launchMonitorThread(); } } else { if (oldListener != null) { removeEventListeners(); } } } /* * (non-Javadoc) * * @see com.windowtester.runtime2.monitor.IUIThreadMonitor#expectDelay(long) */ public void expectDelay(long millis) { long currentTime = System.currentTimeMillis(); synchronized (_lock) { _uiBusyUntil = currentTime + Math.max(millis, _defaultExpectedDelay); } trace("expect delay ", millis); } /* * (non-Javadoc) * * @see com.windowtester.runtime2.monitor.IUIThreadMonitor#setDefaultExpectedDelay(long) */ public void setDefaultExpectedDelay(long millis) { synchronized (_lock) { long currentTime = System.currentTimeMillis(); synchronized (_lock) { _defaultExpectedDelay = millis; _uiBusyUntil = currentTime + _defaultExpectedDelay; } } trace("default delay ", _defaultExpectedDelay); } /** * Notify the listener that the user interface thread has been idle or unresponsive * longer than expected. * * @param isResponsive <code>true</code> if the UI thread is responsive to new user * interface events, {@link org.eclipse.swt.widgets.Display#synchExec} and * {@link org.eclipse.swt.widgets.Display#asyncExec}, and * <code>false</code> if the user interface has not processed any new * events recently as may be hung. */ private void fireUITimeout(final boolean isResponsive) { final IUIThreadMonitorListener listener = getListener(); // log current thread state before triggering shutdown stackTraces = ThreadUtil.getStackTraces(); screenCapture = ScreenCapture.createScreenCapture("UIThreadMonitor-timeout"); Logger.log("UIThreadMonitor: timeout, current thread state\n", stackTraces); if (listener != null) { final Thread thread = new Thread("UIThreadMonitor Notify") { public void run() { listener.uiTimeout(isResponsive); } }; thread.setPriority(Thread.MIN_PRIORITY); thread.setDaemon(true); thread.start(); } } /** * Attempt to gracefully exit the application * * @param isResponsive <code>true</code> if the UI thread is responsive to new user * input, and <code>false</code> if the user interface has not processed * any new events recently and may be hung. */ protected void gracefulExit(boolean isResponsive) { generateResults(isResponsive); // TODO [author=Dan] How do we gracefully exit? forcedExit(isResponsive); } /** * When all else fails, exit the system * * @param isResponsive <code>true</code> if the UI thread is responsive to new user * input, and <code>false</code> if the user interface has not processed * any new events recently and may be hung. */ protected void forcedExit(boolean isResponsive) { String exitMsg = "monitor system exit, isResponsive=" + isResponsive; Logger.log(exitMsg); trace(exitMsg, null); System.exit(9); } // ////////////////////////////////////////////////////////////////////////// // // Internal // // ////////////////////////////////////////////////////////////////////////// /** * Launch a background thread (minimum priority) that checks to see if the UI is still * alive and processing input. */ private void launchMonitorThread() { final Thread monitorThread = new Thread("UIThreadMonitor") { public void run() { initExpectedDelay(); trace("monitor start", null); int checkEventsCount = 0; int uiTimeoutCount = 0; while (true) { if (hasTestEnded()) { trace("monitor end 1", null); break; } checkEventsProcessed(); if (hasTestEnded()) { trace("monitor end 2", null); break; } if (wereEventsProcessed()) { trace("events processed", null); checkEventsCount = 0; uiTimeoutCount = 0; continue; } checkEventsCount++; if (checkEventsCount <= 10) { trace("no events process " + checkEventsCount, null); continue; } boolean isResponsive = isUIThreadResponsive(); if (hasTestEnded()) { trace("monitor end 3", null); break; } if (isResponsive && processConditions()) { trace("conditions handled", null); continue; } if (isDelayExpected()) { trace("delay expected", null); continue; } trace("delay exceeded " + uiTimeoutCount, null); // Take increasingly more drastic steps // based upon how long the delay continues switch (uiTimeoutCount) { case 0 : fireUITimeout(isResponsive); break; case 1 : gracefulExit(isResponsive); break; default : case 2 : forcedExit(isResponsive); } checkEventsCount = 0; uiTimeoutCount++; } } }; monitorThread.setPriority(Thread.MIN_PRIORITY); monitorThread.setDaemon(true); monitorThread.start(); } /** * Initialize the expected delay based upon the default expected delay. */ private void initExpectedDelay() { long currentTime = System.currentTimeMillis(); synchronized (_lock) { _uiBusyUntil = Math.max(_uiBusyUntil, currentTime + _defaultExpectedDelay); } } /** * Determine if the idle or unresponsiveness is expected * * @return <code>true</code> if expected, or <code>false</code> if the user * interface thread has been idle or unresponsive longer than expected */ private boolean isDelayExpected() { long currentTime = System.currentTimeMillis(); synchronized (_lock) { return currentTime < _uiBusyUntil; } } /** * Process condition handlers. * * @return <code>true</code> if at least one condition was handled */ private boolean processConditions() { trace("processing conditions", null); IConditionMonitor monitor = (IConditionMonitor) _uiContext.getAdapter(IConditionMonitor.class); return monitor.process(_uiContext) != IConditionMonitor.PROCESS_NONE; } /** * Check if events were processed during the last call to * {@link #checkEventsProcessed()}. * * @return <code>true</code> if events were processed, else <code>false</code> */ private boolean wereEventsProcessed() { synchronized (_lock) { return _uiEventProcessed; } } /** * Setup a test to see if the UI thread is still processing events. Assume that event * listeners have already been added to update _uiEventProcessed. Use * {@link #wereEventsProcessed()} to determine if events were processed, but check * {@link #hasTestEnded()} first. */ private void checkEventsProcessed() { synchronized (_lock) { _uiEventProcessed = false; } try { Thread.sleep(1000); } catch (InterruptedException e) { // ignored } } /** * Called by subclasses to indicate that events were processed and to adjust the * expected busy time. */ protected void markEventProcessed() { long currentTime = System.currentTimeMillis(); synchronized (_lock) { _uiEventProcessed = true; _uiBusyUntil = Math.max(_uiBusyUntil, currentTime + _defaultExpectedDelay); } } /** * Called by subclasses to indicate that the UI thread was responsive for the current * period of time. */ protected void markUIThreadResponsive() { synchronized (_lock) { _uiThreadResponsive = true; } } // ////////////////////////////////////////////////////////////////////////// // // Implemented by subclasses // // ////////////////////////////////////////////////////////////////////////// /** * Add event listeners to monitor events being processed by the UI. */ protected abstract void addEventListeners(); /** * Remove the event listeners added to monitor events being processed by the UI. */ protected abstract void removeEventListeners(); /** * Wait for up to one second to determine if the user interface thread is responsive * and processing new events. * * @return <code>true</code> if the user interface thread is responsive, or * <code>false</code> if it has not processed any new events within the last * one second */ protected abstract boolean isUIThreadResponsive(); /** * Determine if the test has ended. * * @return <code>true</code> if test has ended, else <code>false</code> */ protected abstract boolean hasTestEnded(); // ////////////////////////////////////////////////////////////////////////// // // Tracing // // ////////////////////////////////////////////////////////////////////////// /** * FOR TESTING PURPOSES ONLY! Enable or diable console level tracing for testing. */ public static void setConsoleTracing(boolean enabled) { _traceMode = enabled ? TRACE_CONSOLE : TRACE_UNINITIALIZED; } /** * Tracing support * * @param message the trace message * @param value additional trace information */ protected void trace(String message, long value) { if (_traceMode == TRACE_OFF) return; trace(message, new Long(value)); } /** * Tracing support * * @param message the trace message * @param value additional trace information or <code>null</code> if none */ protected void trace(String message, Object value) { // TODO [author=Dan] move functionality into new instance of existing Tracer class // in debug plugin if (_traceMode == TRACE_OFF) return; if (_traceMode == TRACE_UNINITIALIZED) { if (!Tracer.isTracing(IRuntimePluginTraceOptions.UI_THREAD_MONITOR)) { _traceMode = TRACE_OFF; return; } _traceMode = TRACE_ON; } long currentTime = System.currentTimeMillis(); StringBuffer buf = new StringBuffer(100); buf.append("UIThreadMonitor: "); buf.append(currentTime); buf.append(" "); buf.append(_uiBusyUntil); buf.append(" "); buf.append(message); if (value != null) { buf.append(" "); buf.append(value); } if (_traceMode == TRACE_CONSOLE) System.out.println(buf.toString()); else Tracer.trace(IRuntimePluginTraceOptions.UI_THREAD_MONITOR, buf.toString()); } // ////////////////////////////////////////////////////////////////////////// // // Result File Generation // // ////////////////////////////////////////////////////////////////////////// /** * Generate a result file immediately before {@link #gracefulExit(boolean)} terminates * the application under test. * * @param isResponsive <code>true</code> if the UI thread is responsive to new user * input, and <code>false</code> if the user interface has not processed * any new events recently and may be hung. */ protected void generateResults(boolean isResponsive) { String resultPathKey = "test.result.xml"; String resultPath = System.getProperty(resultPathKey); if (resultPath == null) { String message = "The system property " + resultPathKey + " is undefined, so no XML result file generated"; Logger.log(message); Tracer.trace(IRuntimePluginTraceOptions.UI_THREAD_MONITOR, message); return; } PrintWriter writer; try { writer = new PrintWriter(new BufferedWriter(new FileWriter(new File(resultPath)))); } catch (IOException e) { String message = "The system property " + resultPathKey + " is defined, but failed to open " + resultPath; Logger.log(message, e); Tracer.trace(IRuntimePluginTraceOptions.UI_THREAD_MONITOR, message); return; } try { writeResults(writer, isResponsive); } finally { writer.close(); } String message = "The system property " + resultPathKey + " is defined, and the result file generated: " + resultPath; Logger.log(message); Tracer.trace(IRuntimePluginTraceOptions.UI_THREAD_MONITOR, message); } /** * Generate a result file immediately before {@link #gracefulExit(boolean)} terminates * the application under test. * * @param isResponsive <code>true</code> if the UI thread is responsive to new user * input, and <code>false</code> if the user interface has not processed * any new events recently and may be hung. */ protected void writeResults(PrintWriter writer, boolean isResponsive) { String testClassnameKey = "test.classname"; String testClassname = System.getProperty(testClassnameKey); if (testClassname == null) { testClassname = "unknown-test-class"; String message = "The system property " + testClassnameKey + " is undefined, so report will show testcase classname as " + testClassname; Logger.log(message); Tracer.trace(IRuntimePluginTraceOptions.UI_THREAD_MONITOR, message); } String errMsg = "UI thread monitor terminated application "; if (isResponsive) errMsg += "because no UI activity"; else errMsg += "because UI thread was unresponsive"; String errDetails = sanitize(stackTraces); writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"); writer.println("<testsuite errors=\"1\" failures=\"0\" name=\"" + testClassname + "\" tests=\"1\" time=\"1\">"); writer.println(" <properties>"); for (Iterator iter = new TreeSet(System.getProperties().keySet()).iterator(); iter.hasNext();) { String key = (String) iter.next(); String value = sanitize(System.getProperty(key)); writer.println(" <property name=\"" + key + "\" value=\"" + value + "\"/>"); } writer.println(" </properties>"); writer.println(" <testcase classname=\"" + testClassname + "\" name=\"testUnknown\" time=\"1\">"); if (screenCapture != null) writer.println(" <screencapture" + " name=\"" + sanitize(screenCapture.getName()) + "\" href=\"" + sanitize(screenCapture.getParentFile().getName()) + "/" + sanitize(screenCapture.getName()) + "\" file=\"" + sanitize(screenCapture.getAbsolutePath()) + "\"/>"); writer.print(" <error message=\"" + errMsg + "\" type=\"" + RuntimeException.class.getName() + "\">"); writer.print(errDetails); writer.println(" </error>"); writer.println(" </testcase>"); writer.println(" <system-out><![CDATA[]]></system-out>"); writer.println(" <system-err><![CDATA[]]></system-err>"); writer.println("</testsuite>"); } /** * Convert raw strings into strings safe for inclusion in an XML file by replacing * specific characters invalid in an XML file with their expanded counterparts (e.g. * replace " with "). * * @param string the original string (not null) * @return the sanitized string (not null) */ protected String sanitize(String string) { String result; result = replaceAll(string, "\"", """); result = replaceAll(string, "<", "<"); return result; } /** * Replace every occurance within the given string of a substring that matches the * given pattern with the given replacement string. The pattern cannot contain any * wildcards. * * @param string the string in which replacements are made * @param pattern the pattern used to find the substrings to be replaced * @param replacement the string used to replace matching substrings */ protected static String replaceAll(String string, String pattern, String replacement) { int stringLength, patternLength, index; StringBuffer buffer; stringLength = string.length(); patternLength = pattern.length(); if (stringLength == 0 || patternLength == 0) { return string; } buffer = new StringBuffer(); index = 0; while (index < stringLength) { if (string.startsWith(pattern, index)) { buffer.append(replacement); index += patternLength; } else { buffer.append(string.charAt(index)); index++; } } return buffer.toString(); } }