blob: 221e77fe96ecef241e0103921bfc021cbd27f7c3 [file] [log] [blame]
package org.eclipse.jface.text.tests.util;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.junit.Assert;
import org.eclipse.swt.widgets.Display;
/**
* Runs the event loop of the given display until {@link #condition()} becomes
* <code>true</code> or no events have occurred for the supplied timeout.
* Between running the event loop, {@link Display#sleep()} is called.
* <p>
* There is a caveat: the given timeouts must be long enough that the calling
* thread can enter <code>Display.sleep()</code> before the timeout elapses,
* otherwise, the waiter may time out before <code>sleep</code> is called and
* the sleeping thread may never be waken up.
* </p>
*
* @since 3.11
*/
public abstract class DisplayHelper {
// copy of org.eclipse.jdt.testplugin.util.DisplayHelper in org.eclipse.jdt.ui.tests
/**
* Controls if the timeout is used. For debugging use true, false otherwise
*/
private static final boolean DISABLE_TIMEOUT= false;
/**
* Creates a new instance.
*/
protected DisplayHelper() {
}
/**
* Until {@link #condition()} becomes <code>true</code> or the timeout
* elapses, call {@link Display#sleep()} and run the event loop.
* <p>
* If <code>timeout &lt; 0</code>, the event loop is never driven and
* only the condition is checked. If <code>timeout == 0</code>, the event
* loop is driven at most once, but <code>Display.sleep()</code> is never
* invoked.
* </p>
*
* @param display the display to run the event loop of
* @param timeout the timeout in milliseconds
* @return <code>true</code> if the condition became <code>true</code>,
* <code>false</code> if the timeout elapsed
*/
public final boolean waitForCondition(Display display, long timeout) {
// if the condition already holds, succeed
if (condition())
return true;
if (timeout < 0)
return false;
// if driving the event loop once makes the condition hold, succeed
// without spawning a thread.
driveEventQueue(display);
if (condition())
return true;
// if the timeout is negative or zero, fail
if (timeout == 0)
return false;
// repeatedly sleep until condition becomes true or timeout elapses
DisplayWaiter waiter= new DisplayWaiter(display);
DisplayWaiter.Timeout timeoutState= waiter.start(timeout);
boolean condition;
try {
do {
if (display.sleep())
driveEventQueue(display);
condition= condition();
} while (!condition && !timeoutState.hasTimedOut());
} finally {
waiter.stop();
}
return condition;
}
/**
* Call {@link Display#sleep()} and run the event loop until the given
* timeout has elapsed.
* <p>
* If <code>timeout &lt; 0</code>, nothing happens. If
* <code>timeout == 0</code>, the event loop is driven exactly once, but
* <code>Display.sleep()</code> is never invoked.
* </p>
*
* @param display the display to run the event loop of
* @param millis the timeout in milliseconds
*/
public static void sleep(Display display, long millis) {
new DisplayHelper() {
@Override
public boolean condition() {
return false;
}
}.waitForCondition(display, millis);
}
/**
* The condition which has to be met in order for {@link #waitForCondition(Display, long)} to
* return before the timeout elapses.
*
* @return <code>true</code> if the condition is met, <code>false</code> if the event loop
* should be driven some more
*/
protected abstract boolean condition();
/**
* Runs the event loop on the given display.
*
* @param display the display
* @return if <code>display.readAndDispatch</code> returned
* <code>true</code> at least once
*/
private static boolean driveEventQueue(Display display) {
boolean events= false;
while (display.readAndDispatch()) {
events= true;
}
return events;
}
/**
* Until {@link #condition()} becomes <code>true</code> or the timeout
* elapses, call {@link Display#sleep()} and run the event loop.
* <p>
* If <code>timeout &lt; 0</code>, the event loop is never driven and
* only the condition is checked. If <code>timeout == 0</code>, the event
* loop is driven at most once, but <code>Display.sleep()</code> is never
* invoked.
* </p>
* <p>
* The condition gets rechecked every <code>interval</code> milliseconds, even
* if no events were read from the queue.
* </p>
*
* @param display the display to run the event loop of
* @param timeout the timeout in milliseconds
* @param interval the interval to re-check the condition in milliseconds
* @return <code>true</code> if the condition became <code>true</code>,
* <code>false</code> if the timeout elapsed
*/
public final boolean waitForCondition(Display display, long timeout, long interval) {
// if the condition already holds, succeed
if (condition())
return true;
if (timeout < 0)
return false;
// if driving the event loop once makes the condition hold, succeed
// without spawning a thread.
driveEventQueue(display);
if (condition())
return true;
// if the timeout is negative or zero, fail
if (timeout == 0)
return false;
// repeatedly sleep until condition becomes true or timeout elapses
DisplayWaiter waiter= new DisplayWaiter(display, true);
long currentTimeMillis= System.currentTimeMillis();
long finalTimeout= timeout + currentTimeMillis;
if (finalTimeout < currentTimeMillis)
finalTimeout= Long.MAX_VALUE;
boolean condition;
try {
do {
waiter.restart(interval);
if (display.sleep())
driveEventQueue(display);
condition= condition();
} while (!condition && (DISABLE_TIMEOUT || finalTimeout > System.currentTimeMillis()));
} finally {
waiter.stop();
}
return condition;
}
}
/**
* Implements the thread that will wait for the timeout and wake up the display
* so it does not wait forever. The thread may be restarted after it was stopped
* or timed out.
*/
final class DisplayWaiter {
/**
* Timeout state of a display waiter thread.
*/
public final class Timeout {
private boolean fTimeoutState= false;
/**
* Returns <code>true</code> if the timeout has been reached,
* <code>false</code> if not.
*
* @return <code>true</code> if the timeout has been reached,
* <code>false</code> if not
*/
public boolean hasTimedOut() {
synchronized (fMutex) {
return fTimeoutState;
}
}
void setTimedOut(boolean timedOut) {
fTimeoutState= timedOut;
}
Timeout(boolean initialState) {
fTimeoutState= initialState;
}
}
// configuration
private final Display fDisplay;
private final Object fMutex= new Object();
private final boolean fKeepRunningOnTimeout;
/* State -- possible transitions:
*
* STOPPED -> RUNNING
* RUNNING -> STOPPED
* RUNNING -> IDLE
* IDLE -> RUNNING
* IDLE -> STOPPED
*/
private static final int RUNNING= 1 << 1;
private static final int STOPPED= 1 << 2;
private static final int IDLE= 1 << 3;
/** The current state. */
private int fState;
/** The time in milliseconds (see Date) that the timeout will occur. */
private long fNextTimeout;
/** The thread. */
private Thread fCurrentThread;
/** The timeout state of the current thread. */
private Timeout fCurrentTimeoutState;
/**
* Creates a new instance on the given display and timeout.
*
* @param display the display to run the event loop of
*/
public DisplayWaiter(Display display) {
this(display, false);
}
/**
* Creates a new instance on the given display and timeout.
*
* @param display the display to run the event loop of
* @param keepRunning <code>true</code> if the thread should be kept
* running after timing out
*/
public DisplayWaiter(Display display, boolean keepRunning) {
Assert.assertNotNull(display);
fDisplay= display;
fState= STOPPED;
fKeepRunningOnTimeout= keepRunning;
}
/**
* Starts the timeout thread if it is not currently running. Nothing happens
* if a thread is already running.
*
* @param delay the delay from now in milliseconds
* @return the timeout state which can be queried for its timed out status
*/
public Timeout start(long delay) {
Assert.assertTrue(delay > 0);
synchronized (fMutex) {
switch (fState) {
case STOPPED:
startThread();
setNextTimeout(delay);
break;
case IDLE:
unhold();
setNextTimeout(delay);
break;
}
return fCurrentTimeoutState;
}
}
/**
* Sets the next timeout to <em>current time</em> plus <code>delay</code>.
*
* @param delay the delay until the next timeout occurs in milliseconds from
* now
*/
private void setNextTimeout(long delay) {
long currentTimeMillis= System.currentTimeMillis();
long next= currentTimeMillis + delay;
if (next > currentTimeMillis)
fNextTimeout= next;
else
fNextTimeout= Long.MAX_VALUE;
}
/**
* Starts the thread if it is not currently running; resets the timeout if
* it is.
*
* @param delay the delay from now in milliseconds
* @return the timeout state which can be queried for its timed out status
*/
public Timeout restart(long delay) {
Assert.assertTrue(delay > 0);
synchronized (fMutex) {
switch (fState) {
case STOPPED:
startThread();
break;
case IDLE:
unhold();
break;
}
setNextTimeout(delay);
return fCurrentTimeoutState;
}
}
/**
* Stops the thread if it is running. If not, nothing happens. Another
* thread may be started by calling {@link #start(long)} or
* {@link #restart(long)}.
*/
public void stop() {
synchronized (fMutex) {
if (tryTransition(RUNNING | IDLE, STOPPED))
fMutex.notifyAll();
}
}
/**
* Puts the reaper thread on hold but does not stop it. It may be restarted
* by calling {@link #start(long)} or {@link #restart(long)}.
*/
public void hold() {
synchronized (fMutex) {
// nothing to do if there is no thread
if (tryTransition(RUNNING, IDLE))
fMutex.notifyAll();
}
}
/**
* Transition to <code>RUNNING</code> and clear the timed out flag. Assume
* current state is <code>IDLE</code>.
*/
private void unhold() {
checkedTransition(IDLE, RUNNING);
fCurrentTimeoutState= new Timeout(false);
fMutex.notifyAll();
}
/**
* Start the thread. Assume the current state is <code>STOPPED</code>.
*/
private void startThread() {
checkedTransition(STOPPED, RUNNING);
fCurrentTimeoutState= new Timeout(false);
fCurrentThread= new Thread() {
/**
* Exception thrown when a thread notices that it has been stopped
* and a new thread has been started.
*/
final class ThreadChangedException extends Exception {
private static final long serialVersionUID= 1L;
}
/*
* @see java.lang.Runnable#run()
*/
@Override
public void run() {
try {
run2();
} catch (InterruptedException e) {
// ignore and end the thread - we never interrupt ourselves,
// so it must be an external entity that interrupted us
Logger.getGlobal().log(Level.FINE, "", e);
} catch (ThreadChangedException e) {
// the thread was stopped and restarted before we got out
// of a wait - we're no longer used
// we might have been notified instead of the current thread,
// so wake it up
Logger.getGlobal().log(Level.FINE, "", e);
synchronized (fMutex) {
fMutex.notifyAll();
}
}
}
/**
* Runs the thread.
*
* @throws InterruptedException if the thread was interrupted
* @throws ThreadChangedException if the thread changed
*/
private void run2() throws InterruptedException, ThreadChangedException {
synchronized (fMutex) {
checkThread();
tryHold(); // wait / potential state change
assertStates(STOPPED | RUNNING);
while (isState(RUNNING)) {
waitForTimeout(); // wait / potential state change
if (isState(RUNNING))
timedOut(); // state change
assertStates(STOPPED | IDLE);
tryHold(); // wait / potential state change
assertStates(STOPPED | RUNNING);
}
assertStates(STOPPED);
}
}
/**
* Check whether the current thread is this thread, throw an
* exception otherwise.
*
* @throws ThreadChangedException if the current thread changed
*/
private void checkThread() throws ThreadChangedException {
if (fCurrentThread != this)
throw new ThreadChangedException();
}
/**
* Waits until the next timeout occurs.
*
* @throws InterruptedException if the thread was interrupted
* @throws ThreadChangedException if the thread changed
*/
private void waitForTimeout() throws InterruptedException, ThreadChangedException {
long delta;
while (isState(RUNNING) && (delta = fNextTimeout - System.currentTimeMillis()) > 0) {
delta= Math.max(delta, 50); // wait at least 50ms in order to avoid timing out before the display is going to sleep
Logger.getGlobal().finest("sleeping for " + delta + "ms");
fMutex.wait(delta);
checkThread();
}
}
/**
* Sets the timed out flag and wakes up the display. Transitions to
* <code>IDLE</code> (if in keep-running mode) or
* <code>STOPPED</code>.
*/
private void timedOut() {
Logger.getGlobal().finer("timed out");
fCurrentTimeoutState.setTimedOut(true);
fDisplay.wake(); // wake up call!
if (fKeepRunningOnTimeout)
checkedTransition(RUNNING, IDLE);
else
checkedTransition(RUNNING, STOPPED);
}
/**
* Waits while the state is <code>IDLE</code>, then returns. The
* state must not be <code>RUNNING</code> when calling this
* method. The state is either <code>STOPPED</code> or
* <code>RUNNING</code> when the method returns.
*
* @throws InterruptedException if the thread was interrupted
* @throws ThreadChangedException if the thread has changed while on
* hold
*/
private void tryHold() throws InterruptedException, ThreadChangedException {
while (isState(IDLE)) {
fMutex.wait(0);
checkThread();
}
assertStates(STOPPED | RUNNING);
}
};
fCurrentThread.start();
}
/**
* Transitions to <code>nextState</code> if the current state is one of
* <code>possibleStates</code>. Returns <code>true</code> if the
* transition happened, <code>false</code> otherwise.
*
* @param possibleStates the states which trigger a transition
* @param nextState the state to transition to
* @return <code>true</code> if the transition happened,
* <code>false</code> otherwise
*/
private boolean tryTransition(int possibleStates, int nextState) {
if (isState(possibleStates)) {
Logger.getGlobal().finer(name(fState) + " > " + name(nextState) + " (" + name(possibleStates) + ")");
fState= nextState;
return true;
}
Logger.getGlobal().finest("noTransition" + name(fState) + " !> " + name(nextState) + " (" + name(possibleStates) + ")");
return false;
}
/**
* Checks the <code>possibleStates</code> and throws an assertion if it is
* not met, then transitions to <code>nextState</code>.
*
* @param possibleStates the allowed states
* @param nextState the state to transition to
*/
private void checkedTransition(int possibleStates, int nextState) {
assertStates(possibleStates);
Logger.getGlobal().finer(name(fState) + " > " + name(nextState));
fState= nextState;
}
/**
* Implements state consistency checking.
*
* @param states the allowed states
* @throws junit.framework.AssertionFailedError if the current state is not
* in <code>states</code>
*/
private void assertStates(int states) {
Assert.assertTrue("illegal state", isState(states));
}
/**
* Answers <code>true</code> if the current state is in the given
* <code>states</code>.
*
* @param states the possible states
* @return <code>true</code> if the current state is in the given states,
* <code>false</code> otherwise
*/
private boolean isState(int states) {
return (states & fState) == fState;
}
/**
* Pretty print the given states.
*
* @param states the states
* @return a string representation of the states
*/
private String name(int states) {
StringBuffer buf= new StringBuffer();
boolean comma= false;
if ((states & RUNNING) == RUNNING) {
buf.append("RUNNING");
comma= true;
}
if ((states & STOPPED) == STOPPED) {
if (comma)
buf.append(",");
buf.append("STOPPED");
comma= true;
}
if ((states & IDLE) == IDLE) {
if (comma)
buf.append(",");
buf.append("IDLE");
}
return buf.toString();
}
}