/*******************************************************************************
* 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();
}
}