Merge branch 'GP-463_dragonmacher_PR-2534_sudofox_exclusive-checkout-typo-fix'

This commit is contained in:
dragonmacher
2020-12-07 12:34:43 -05:00
9 changed files with 806 additions and 331 deletions
@@ -49,6 +49,7 @@ import ghidra.framework.ApplicationConfiguration;
import ghidra.util.*;
import ghidra.util.datastruct.WeakSet;
import ghidra.util.exception.AssertException;
import ghidra.util.task.AbstractSwingUpdateManager;
import ghidra.util.task.SwingUpdateManager;
import junit.framework.AssertionFailedError;
import sun.awt.AppContext;
@@ -450,6 +451,7 @@ public abstract class AbstractGenericTest extends AbstractGTest {
// a more reasonable number
//
// Update: 744 references at 12/1/19
// Update: 559 references at 12/4/20
public static void waitForPostedSwingRunnables() {
waitForSwing();
}
@@ -1594,13 +1596,13 @@ public abstract class AbstractGenericTest extends AbstractGTest {
throw new AssertException("Can't wait for swing from within the swing thread!");
}
Set<SwingUpdateManager> set = new HashSet<>();
Set<AbstractSwingUpdateManager> set = new HashSet<>();
runSwing(() -> {
@SuppressWarnings("unchecked")
WeakSet<SwingUpdateManager> s =
(WeakSet<SwingUpdateManager>) getInstanceField("instances",
SwingUpdateManager.class);
for (SwingUpdateManager manager : s) {
for (AbstractSwingUpdateManager manager : s) {
set.add(manager);
}
});
@@ -1617,7 +1619,7 @@ public abstract class AbstractGenericTest extends AbstractGTest {
return wasEverBusy;
}
private static boolean waitForSwing(Set<SwingUpdateManager> managers, boolean flush) {
private static boolean waitForSwing(Set<AbstractSwingUpdateManager> managers, boolean flush) {
// Note: not sure how long is too long to wait for the Swing thread and update managers
// to finish. This is usually less than a second. We have seen a degenerate
@@ -1637,7 +1639,7 @@ public abstract class AbstractGenericTest extends AbstractGTest {
yieldToSwing();
keepGoing = false;
for (SwingUpdateManager manager : managers) {
for (AbstractSwingUpdateManager manager : managers) {
if (!manager.isBusy()) {
// no current or pending work
@@ -1674,7 +1676,7 @@ public abstract class AbstractGenericTest extends AbstractGTest {
return wasEverBusy;
}
private static void flushAllManagers(Set<SwingUpdateManager> managers, boolean flush) {
private static void flushAllManagers(Set<AbstractSwingUpdateManager> managers, boolean flush) {
//
// Some update managers will make an update that causes another manager to schedule an
@@ -1691,21 +1693,19 @@ public abstract class AbstractGenericTest extends AbstractGTest {
// which would be 2
int n = 3;
for (int i = 0; i < n; i++) {
for (SwingUpdateManager manager : managers) {
for (AbstractSwingUpdateManager manager : managers) {
doFlush(flush, manager);
}
}
}
private static void doFlush(boolean doFlush, SwingUpdateManager manager) {
private static void doFlush(boolean doFlush, AbstractSwingUpdateManager manager) {
if (!doFlush) {
return;
}
runSwing(() -> {
if (manager.hasPendingUpdates()) {
manager.updateNow();
}
manager.flush();
}, false);
yieldToSwing();
}
@@ -0,0 +1,362 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.util.task;
import javax.swing.Timer;
import ghidra.util.Msg;
import ghidra.util.SystemUtilities;
import ghidra.util.datastruct.WeakDataStructureFactory;
import ghidra.util.datastruct.WeakSet;
import utilities.util.reflection.ReflectionUtilities;
/**
* A base class to allow clients to buffer events. UI components may receive numbers events to make
* changes to their underlying data model. Further, for many of these clients, it is sufficient
* to perform one update to capture all of the changes. In this scenario, the client can use this
* class to keep pushing off internal updates until: 1) the flurry of events has settled down, or
* 2) some specified amount of time has expired.
* <p>
* The various methods dictate when the client will get a callback:<p>
* <ul>
* <li>{@link #update()} - if this is the first call to <code>update</code>, then do the work
* immediately; otherwise, buffer the update request until the
* timeout has expired.</li>
* <li>{@link #updateNow()} - perform the callback now.</li>
* <li>{@link #updateLater()} - buffer the update request until the timeout has expired.</li>
* <li>Non-blocking update now - this is a conceptual use-case, where the client wishes to perform an
* immediate update, but not during the current Swing event. To achieve
* this, you could call something like:
* <pre>{@literal
* SwingUtilities.invokeLater(() -> updateManager.updateNow());
* }</pre>
* </li>
* </ul>
*
* <P> This class is safe to use in a multi-threaded environment. State variables are guarded
* via synchronization on this object. The Swing thread is used to perform updates, which
* guarantees that only one update will happen at a time.
*/
public abstract class AbstractSwingUpdateManager {
protected static final long NONE = 0;
public static final int DEFAULT_MAX_DELAY = 30000;
protected static final int MIN_DELAY_FLOOR = 10;
protected static final int DEFAULT_MIN_DELAY = 250;
protected static final String DEFAULT_NAME = AbstractSwingUpdateManager.class.getSimpleName();
private static final WeakSet<AbstractSwingUpdateManager> instances =
WeakDataStructureFactory.createCopyOnReadWeakSet();
protected final Timer timer;
private final int minDelay;
private final int maxDelay;
private final String name;
private String inceptionInformation;
protected long requestTime = NONE;
protected long bufferingStartTime;
protected boolean disposed = false;
// This is true when work has begun and is not finished. This is only mutated on the
// Swing thread, but is read by other threads.
protected boolean isWorking;
/**
* Constructs a new SwingUpdateManager with default values for min and max delay. See
* {@link #DEFAULT_MIN_DELAY} and {@value #DEFAULT_MAX_DELAY}.
*/
protected AbstractSwingUpdateManager() {
this(DEFAULT_MIN_DELAY, DEFAULT_MAX_DELAY);
}
/**
* Constructs a new AbstractSwingUpdateManager
* <p>
* <b>Note: </b>The <code>minDelay</code> will always be at least {@link #MIN_DELAY_FLOOR},
* regardless of the given value.
*
* @param minDelay the minimum number of milliseconds to wait once the event stream stops
* coming in before actually updating the screen
*/
protected AbstractSwingUpdateManager(int minDelay) {
this(minDelay, DEFAULT_MAX_DELAY);
}
/**
* Constructs a new AbstractSwingUpdateManager
* <p>
* <b>Note: </b>The <code>minDelay</code> will always be at least {@link #MIN_DELAY_FLOOR},
* regardless of the given value.
*
* @param minDelay the minimum number of milliseconds to wait once the event stream stops
* coming in before actually updating the screen.
* @param maxDelay the maximum amount of time to wait between gui updates.
*/
protected AbstractSwingUpdateManager(int minDelay, int maxDelay) {
this(minDelay, maxDelay, DEFAULT_NAME);
}
/**
* Constructs a new AbstractSwingUpdateManager
* <p>
* <b>Note: </b>The <code>minDelay</code> will always be at least {@link #MIN_DELAY_FLOOR},
* regardless of the given value.
*
* @param minDelay the minimum number of milliseconds to wait once the event stream stops
* coming in before actually updating the screen.
* @param maxDelay the maximum amount of time to wait between gui updates.
* @param name The name of this update manager; this allows for selective trace logging
*/
protected AbstractSwingUpdateManager(int minDelay, int maxDelay, String name) {
this.maxDelay = maxDelay;
this.name = name;
recordInception();
this.minDelay = Math.max(MIN_DELAY_FLOOR, minDelay);
timer = new Timer(minDelay, e -> timerCallback());
timer.setRepeats(false);
instances.add(this);
}
/**
* The subclass callback to perform work.
*/
protected abstract void swingDoWork();
/**
* Signals to perform an update. See the class header for the usage of the various
* update methods.
*/
protected synchronized void update() {
if (disposed) {
return;
}
requestTime = System.currentTimeMillis();
SystemUtilities.runSwingLater(this::checkForWork);
}
/**
* Signals to perform an update. See the class header for the usage of the various
* update methods.
*/
protected synchronized void updateLater() {
if (disposed) {
return;
}
requestTime = System.currentTimeMillis();
bufferingStartTime = bufferingStartTime == NONE ? requestTime : bufferingStartTime;
scheduleCheckForWork();
}
/**
* Signals to perform an update. See the class header for the usage of the various
* update methods.
*/
protected void updateNow() {
synchronized (this) {
if (disposed) {
return;
}
requestTime = System.currentTimeMillis();
bufferingStartTime = NONE; // set so that the max delay check will trigger work
}
SystemUtilities.runSwingNow(this::checkForWork);
}
/**
* Causes this run manager to run if it has a pending update
*/
public void flush() {
if (hasPendingUpdates()) {
SystemUtilities.runSwingNow(this::checkForWork);
}
}
/**
* Signals to stop any buffered work. This will not stop any in-progress work.
*/
public synchronized void stop() {
if (disposed) {
return;
}
timer.stop();
requestTime = NONE;
bufferingStartTime = NONE;
}
/**
* Returns true if there is a pending request that hasn't started yet. Any currently
* executing requests will not affect this call.
*
* @return true if there is a pending request that hasn't started yet.
*/
public synchronized boolean hasPendingUpdates() {
if (disposed) {
return false;
}
return requestTime != NONE;
}
/**
* Returns true if any work is being performed or if there is buffered work
* @return true if any work is being performed or if there is buffered work
*/
public synchronized boolean isBusy() {
if (disposed) {
return false;
}
return requestTime != NONE || isWorking;
}
public synchronized void dispose() {
timer.stop();
instances.remove(this);
requestTime = NONE;
bufferingStartTime = NONE;
disposed = true;
}
public synchronized boolean isDisposed() {
return disposed;
}
@Override
public String toString() {
return name + " @ " + inceptionInformation;
}
public String toStringDebug() {
//@formatter:off
return "{\n" +
"\tname: " + name + "\n" +
"\tcreator: " + inceptionInformation + " ("+System.identityHashCode(this)+")\n" +
"\trequest time: "+requestTime + "\n" +
"\twork count: " + isWorking + "\n" +
"}";
//@formatter:on
}
// note: this is called on the Swing thread
protected void checkForWork() {
if (shouldDoWork()) {
swingExecutePendingWork();
}
}
// This is similar to checkForWork except that it resets the task buffering when
// the time expires and there is no work to do.
private void timerCallback() {
if (shouldDoWork()) {
swingExecutePendingWork();
}
else if (requestTime == NONE) {
bufferingStartTime = NONE; // The timer has fired and there is no pending work
}
}
// note: this is called on the Swing thread
private synchronized boolean shouldDoWork() {
// If no pending request, exit without restarting timer
if (requestTime == NONE) {
return false;
}
long now = System.currentTimeMillis();
if (isTimeToWork(now)) {
bufferingStartTime = now;
requestTime = NONE;
isWorking = true;
return true;
}
scheduleCheckForWork();
return false;
}
protected void scheduleCheckForWork() {
timer.start();
}
private boolean isTimeToWork(long now) {
// if past maximum delay, always do work
long timeSinceBufferingStart = now - bufferingStartTime;
if (timeSinceBufferingStart > maxDelay) {
return true;
}
// if no new requests have come in since the last time we checked, do work
long timeSinceLastRequest = now - requestTime;
if (timeSinceLastRequest > minDelay) {
return true;
}
return false;
}
// note: this is called on the Swing thread
private void swingExecutePendingWork() {
try {
swingDoWork();
}
catch (Throwable t) {
// catch exceptions so we don't kill the timer
Msg.showError(this, null, "Unexpected Exception",
"Unexpected exception in Swing Update Manager", t);
}
isWorking = false;
// we need to clear the buffering flag after the minDelay has passed, so start the timer
scheduleCheckForWork();
}
//==================================================================================================
// Inception Info
//==================================================================================================
private void recordInception() {
inceptionInformation = getInceptionFromTheFirstClassThatIsNotUs();
}
private String getInceptionFromTheFirstClassThatIsNotUs() {
Throwable t = ReflectionUtilities.createThrowableWithStackOlderThan(getClass());
StackTraceElement[] trace = t.getStackTrace();
String classInfo = trace[0].toString();
/*
// debug source of creation
Throwable filtered = ReflectionUtilities.filterJavaThrowable(t);
String string = ReflectionUtilities.stackTraceToString(filtered);
classInfo = classInfo + "\n\tfrom:\n\n" + string;
*/
return classInfo;
}
}
@@ -0,0 +1,82 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.util.task;
import utility.function.Dummy;
/**
* A class that run the client's runnable on the Swing thread. Repeated requests will get buffered
* until the max delay is reached.
*/
public class BufferedSwingRunner extends AbstractSwingUpdateManager {
private Runnable nextRunnable;
/**
* Constructs a new SwingUpdateManager
* <p>
* <b>Note: </b>The <code>minDelay</code> will always be at least {@link #MIN_DELAY_FLOOR},
* regardless of the given value.
*
* @param minDelay the minimum number of milliseconds to wait once the event stream stops
* coming in before actually updating the screen.
* @param maxDelay the maximum amount of time to wait between gui updates.
*/
public BufferedSwingRunner(int minDelay, int maxDelay) {
super(minDelay, maxDelay, DEFAULT_NAME);
}
public BufferedSwingRunner() {
super(DEFAULT_MIN_DELAY, DEFAULT_MAX_DELAY, DEFAULT_NAME);
}
@Override
protected void swingDoWork() {
Runnable currentRunnable = prepareCurrentRunnable();
currentRunnable.run();
}
/**
* Runs the given runnable. If this is the first call to <code>run</code>, then do the work
* immediately; otherwise, buffer the request until the timeout has expired.
*
* <p>See the header of {@link AbstractSwingUpdateManager} for details on the update process.
*
* @param r the task to run on the Swing thread
*/
public synchronized void run(Runnable r) {
this.nextRunnable = r;
update();
}
/**
* Runs the given runnable later, buffering the request until the timeout has expired.
*
* <p>See the header of {@link AbstractSwingUpdateManager} for details on the update process.
*
* @param r the task to run on the Swing thread
*/
public synchronized void runLater(Runnable r) {
this.nextRunnable = r;
updateLater();
}
private synchronized Runnable prepareCurrentRunnable() {
Runnable currentRunnable = nextRunnable;
nextRunnable = null;
return Dummy.ifNull(currentRunnable);
}
}
@@ -15,13 +15,7 @@
*/
package ghidra.util.task;
import javax.swing.Timer;
import ghidra.util.Msg;
import ghidra.util.SystemUtilities;
import ghidra.util.datastruct.WeakDataStructureFactory;
import ghidra.util.datastruct.WeakSet;
import utilities.util.reflection.ReflectionUtilities;
import ghidra.util.Swing;
/**
* A class to allow clients to buffer events. UI components may receive numbers events to make
@@ -50,31 +44,10 @@ import utilities.util.reflection.ReflectionUtilities;
* via synchronization on this object. The Swing thread is used to perform updates, which
* guarantees that only one update will happen at a time.
*/
public class SwingUpdateManager {
private static final long NONE = 0;
public static final int DEFAULT_MAX_DELAY = 30000;
private static final int MIN_DELAY_FLOOR = 10;
private static final int DEFAULT_MIN_DELAY = 250;
private static final String DEFAULT_NAME = SwingUpdateManager.class.getSimpleName();
private static final WeakSet<SwingUpdateManager> instances =
WeakDataStructureFactory.createCopyOnReadWeakSet();
public class SwingUpdateManager extends AbstractSwingUpdateManager {
private final Timer timer;
private final int minDelay;
private final int maxDelay;
private final Runnable clientRunnable;
private final String name;
private String inceptionInformation;
private long requestTime = NONE;
private long bufferingStartTime;
private boolean disposed = false;
// This is true when work has begun and is not finished. This is only mutated on the
// Swing thread, but is read by other threads.
private boolean isWorking;
/**
* Constructs a new SwingUpdateManager with default values for min and max delay. See
* {@link #DEFAULT_MIN_DELAY} and {@value #DEFAULT_MAX_DELAY}.
@@ -88,8 +61,8 @@ public class SwingUpdateManager {
/**
* Constructs a new SwingUpdateManager
* <p>
* <b>Note: </b>The <code>minDelay</code> will always be at least {@link #MIN_DELAY_FLOOR}, regardless of
* the given value.
* <b>Note: </b>The <code>minDelay</code> will always be at least {@link #MIN_DELAY_FLOOR},
* regardless of the given value.
*
* @param minDelay the minimum number of milliseconds to wait once the event stream stops
* coming in before actually updating the screen.
@@ -102,8 +75,8 @@ public class SwingUpdateManager {
/**
* Constructs a new SwingUpdateManager
* <p>
* <b>Note: </b>The <code>minDelay</code> will always be at least {@link #MIN_DELAY_FLOOR}, regardless of
* the given value.
* <b>Note: </b>The <code>minDelay</code> will always be at least {@link #MIN_DELAY_FLOOR},
* regardless of the given value.
*
* @param minDelay the minimum number of milliseconds to wait once the event stream stops
* coming in before actually updating the screen.
@@ -111,7 +84,8 @@ public class SwingUpdateManager {
* @param r the runnable that performs the client work.
*/
public SwingUpdateManager(int minDelay, int maxDelay, Runnable r) {
this(minDelay, maxDelay, DEFAULT_NAME, r);
super(minDelay, maxDelay, DEFAULT_NAME);
this.clientRunnable = r;
}
/**
@@ -127,39 +101,34 @@ public class SwingUpdateManager {
* @param r the runnable that performs the client work.
*/
public SwingUpdateManager(int minDelay, int maxDelay, String name, Runnable r) {
if (r == null) {
throw new IllegalArgumentException("Runnable callback cannot be null");
}
super(minDelay, maxDelay, name);
this.clientRunnable = r;
this.maxDelay = maxDelay;
this.name = name;
}
recordInception();
this.minDelay = Math.max(MIN_DELAY_FLOOR, minDelay);
timer = new Timer(minDelay, e -> timerCallback());
timer.setRepeats(false);
instances.add(this);
@Override
protected void swingDoWork() {
clientRunnable.run();
}
/**
* Signals to perform an update. See the class header for the usage of the various
* update methods.
*/
@Override
public synchronized void update() {
if (disposed) {
return;
}
requestTime = System.currentTimeMillis();
SystemUtilities.runSwingLater(this::checkForWork);
Swing.runLater(this::checkForWork);
}
/**
* Signals to perform an update. See the class header for the usage of the various
* update methods.
*/
@Override
public synchronized void updateLater() {
if (disposed) {
return;
@@ -174,6 +143,7 @@ public class SwingUpdateManager {
* Signals to perform an update. See the class header for the usage of the various
* update methods.
*/
@Override
public void updateNow() {
synchronized (this) {
if (disposed) {
@@ -183,176 +153,7 @@ public class SwingUpdateManager {
requestTime = System.currentTimeMillis();
bufferingStartTime = NONE; // set so that the max delay check will trigger work
}
SystemUtilities.runSwingNow(this::checkForWork);
}
/**
* Signals to stop any buffered work. This will not stop any in-progress work.
*/
public synchronized void stop() {
if (disposed) {
return;
}
timer.stop();
requestTime = NONE;
bufferingStartTime = NONE;
}
/**
* Returns true if there is a pending request that hasn't started yet. Any currently
* executing requests will not affect this call.
*
* @return true if there is a pending request that hasn't started yet.
*/
public synchronized boolean hasPendingUpdates() {
if (disposed) {
return false;
}
return requestTime != NONE;
}
/**
* Returns true if any work is being performed or if there is buffered work
* @return true if any work is being performed or if there is buffered work
*/
public synchronized boolean isBusy() {
if (disposed) {
return false;
}
return requestTime != NONE || isWorking;
}
public synchronized void dispose() {
timer.stop();
instances.remove(this);
requestTime = NONE;
bufferingStartTime = NONE;
disposed = true;
}
public synchronized boolean isDisposed() {
return disposed;
}
@Override
public String toString() {
return name + " @ " + inceptionInformation;
}
public String toStringDebug() {
//@formatter:off
return "{\n" +
"\tname: " + name + "\n" +
"\tcreator: " + inceptionInformation + " ("+System.identityHashCode(this)+")\n" +
"\trequest time: "+requestTime + "\n" +
"\twork count: " + isWorking + "\n" +
"}";
//@formatter:on
}
// note: this is called on the Swing thread
private void checkForWork() {
if (shouldDoWork()) {
doWork();
}
}
// This is similar to checkForWork except that it resets the task buffering when
// the time expires and there is no work to do.
private void timerCallback() {
if (shouldDoWork()) {
doWork();
}
else if (requestTime == NONE) {
bufferingStartTime = NONE; // The timer has fired and there is no pending work
}
}
// note: this is called on the Swing thread
private synchronized boolean shouldDoWork() {
// If no pending request, exit without restarting timer
if (requestTime == NONE) {
return false;
}
long now = System.currentTimeMillis();
if (isTimeToWork(now)) {
bufferingStartTime = now;
requestTime = NONE;
isWorking = true;
return true;
}
scheduleCheckForWork();
return false;
}
private void scheduleCheckForWork() {
timer.start();
}
private boolean isTimeToWork(long now) {
// if past maximum delay, always do work
long timeSinceBufferingStart = now - bufferingStartTime;
if (timeSinceBufferingStart > maxDelay) {
return true;
}
// if no new requests have come in since the last time we checked, do work
long timeSinceLastRequest = now - requestTime;
if (timeSinceLastRequest > minDelay) {
return true;
}
return false;
}
// note: this is called on the Swing thread
private void doWork() {
try {
clientRunnable.run();
}
catch (Throwable t) {
// catch exceptions so we don't kill the timer
Msg.showError(this, null, "Unexpected Exception",
"Unexpected exception in Swing Update Manager", t);
}
isWorking = false;
// we need to clear the buffering flag after the minDelay has passed, so start the timer
scheduleCheckForWork();
}
//==================================================================================================
// Inception Info
//==================================================================================================
private void recordInception() {
inceptionInformation = getInceptionFromTheFirstClassThatIsNotUs();
}
private String getInceptionFromTheFirstClassThatIsNotUs() {
Throwable t = ReflectionUtilities.createThrowableWithStackOlderThan(getClass());
StackTraceElement[] trace = t.getStackTrace();
String classInfo = trace[0].toString();
/*
// debug source of creation
Throwable filtered = ReflectionUtilities.filterJavaThrowable(t);
String string = ReflectionUtilities.stackTraceToString(filtered);
classInfo = classInfo + "\n\tfrom:\n\n" + string;
*/
return classInfo;
Swing.runLater(this::checkForWork);
}
}
@@ -20,6 +20,7 @@ import java.io.Serializable;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.core.*;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.config.Property;
import org.apache.logging.log4j.core.config.plugins.*;
import org.apache.logging.log4j.core.layout.PatternLayout;
@@ -41,19 +42,20 @@ public class LogPanelAppender extends AbstractAppender {
private LogListener logListener;
protected LogPanelAppender(String name, Filter filter, Layout<? extends Serializable> layout) {
super(name, filter, layout);
super(name, filter, layout, true, Property.EMPTY_ARRAY);
}
@Override
public void append(LogEvent event) {
if (logListener == null) {
return;
}
// An error is identified as any log that is tagged ERROR or FATAL.
boolean isError = event.getLevel().isMoreSpecificThan(Level.ERROR);
String message = event.getMessage().getFormattedMessage();
if (logListener != null) {
logListener.messageLogged(message, isError);
}
logListener.messageLogged(message, isError);
}
@PluginFactory
@@ -0,0 +1,282 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.util.task;
import static org.junit.Assert.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Before;
import org.junit.Test;
import generic.test.AbstractGenericTest;
import ghidra.util.SystemUtilities;
public class BufferedSwingRunnerTest extends AbstractGenericTest {
private static final int MIN_DELAY = 500;
private static final int MAX_DELAY = 1000;
private volatile int runnableCalled;
private BufferedSwingRunner runner = new BufferedSwingRunner(MIN_DELAY, MAX_DELAY);
private ClientRunnable runnable = new ClientRunnable();
@Before
public void setUp() throws Exception {
// must turn this on to get the expected results, as in headless mode the update manager
// will run it's Swing work immediately on the test thread, which is not true to the
// default behavior
System.setProperty(SystemUtilities.HEADLESS_PROPERTY, Boolean.FALSE.toString());
}
@Test
public void testOneCallForOneRun() {
runner.run(runnable);
waitForRunner();
assertEquals("Expected only 1 callback", 1, runnableCalled);
}
@Test
public void testTwoCallForMultipleFastRunCalls() {
for (int i = 0; i < 10; i++) {
runner.run(runnable);
sleep(10);
}
waitForRunner();
assertEquals("Expected 2 callbacks", 2, runnableCalled);
}
@Test
public void testOneCallForRunLater() {
runner.runLater(runnable);
waitForRunner();
assertEquals("Expected only 1 callback", 1, runnableCalled);
}
@Test
public void testMaxTimeout() {
//
// Hard Test!: Many timing-sensitive factors are at play here: this thread and it's
// run and sleep scheduling; the Swing thread and its scheduling and the
// Swing timer's scheduling. Thus, making a simple AND reliable test is
// nearly impossible. So, we test here the basic max timeout mechanism.
//
// The loop below calls update many times. The first call to update always
// triggers an immediate update. Each successive call, as long as it is
// faster than *min delay* will trigger an update to be buffered, to be
// delayed. This delay gets reset upon each update call until the *max delay*
// time has been passed.
//
int sleepyTime = 5;
int maxDelay = 600;
runner = new BufferedSwingRunner(200, maxDelay);
for (int i = 0; i < 4; i++) {
runner.run(runnable);
sleep(sleepyTime);
}
assertEquals("Expected 1 max delay callback", 1, runnableCalled);
waitForRunner();
assertEquals("Expected one immediate callback and one max delay callback (2 total)", 2,
runnableCalled);
}
@Test
public void testFlush() {
runner.flush();
waitForSwing();
assertFalse(runner.isBusy());
assertEquals("Did not expect the callback after stop()", 0, runnableCalled);
runner.runLater(runnable);
runner.flush();
waitForRunner();
assertEquals("Expected only 1 callback", 1, runnableCalled);
}
@Test
public void testStop() {
runner.runLater(runnable);
runner.stop();
waitForSwing();
assertFalse(runner.isBusy());
assertEquals("Did not expect the callback after stop()", 0, runnableCalled);
//
// Make sure we can use again
//
runnableCalled = 0;
runner.run(runnable);
waitForRunner();
assertEquals("Expected only 1 callback", 1, runnableCalled);
}
@Test
public void testHasPendingUpdates() {
runner.runLater(runnable);
assertTrue("Should have pending updates after calling update, but before the work" +
"has been done", runner.hasPendingUpdates());
waitForRunner();
assertEquals("Expected only 1 callback", 1, runnableCalled);
assertFalse("Still have pending updates after performing work",
runner.hasPendingUpdates());
}
@Test
public void testDispose() {
for (int i = 0; i < 10; i++) {
runner.run(runnable);
sleep(1);
}
assertTrue(runner.hasPendingUpdates());
runner.dispose();
assertFalse(runner.hasPendingUpdates());
int called = runnableCalled;
//
// Cannot use again
//
runner.run(runnable);
waitForRunner();
// make sure the callback did not occur.
assertEquals(called, runnableCalled);
}
@Test
public void testIsBusy() throws Exception {
//
// Another complicated test: we want to make sure that if we try to trigger an update
// that isBusy() will return true, even if the update runnable has not yet been posted
// to the Swing thread.
//
// To do this, we will need to block the Swing thread so that we know the request happens
// before the update runnable is called.
//
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(1);
AtomicBoolean exception = new AtomicBoolean();
runSwing(new Runnable() {
@Override
public void run() {
startLatch.countDown();
try {
endLatch.await(10, TimeUnit.SECONDS);
}
catch (InterruptedException e) {
exception.set(true);
}
}
}, false);
// This will cause the swing thread to block until we countdown the end latch
startLatch.await(10, TimeUnit.SECONDS);
runner.run(runnable);
assertTrue("Manager not busy after requesting an update", runner.isBusy());
endLatch.countDown();
waitForRunner();
assertFalse("Manager still busy after waiting for update", runner.isBusy());
assertFalse("Interrupted waiting for CountDowLatch", exception.get());
}
@Test
public void testCallToUpdateWhileAnUpdateIsWorking() throws Exception {
//
// Test that an update call from a non-swing thread will still get processed if the
// manager is actively processing an update on the swing thread.
//
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(1);
AtomicBoolean exception = new AtomicBoolean();
Runnable r = () -> {
runnableCalled++;
startLatch.countDown();
try {
endLatch.await(10, TimeUnit.SECONDS);
}
catch (InterruptedException e) {
exception.set(true);
}
};
// start the update manager and have it wait for us
runner.run(r);
// have the swing thread block until we countdown the end latch
startLatch.await(10, TimeUnit.SECONDS);
// post the second update request now that the manager is actively processing
runner.run(runnable);
// let the update manager finish; verify 2 work items total
endLatch.countDown();
waitForRunner();
assertEquals("Expected exactly 2 callbacks", 2, runnableCalled);
}
@Test
public void testNotFiringTooOften() throws InterruptedException {
Thread t = new Thread(() -> {
for (int i = 0; i < 50; i++) {
runner.run(runnable);
runner.run(runnable);
runner.run(runnable);
runner.run(runnable);
sleep(10);
}
});
t.start();
t.join();
waitForRunner();
assertEquals(2, runnableCalled);
}
//==================================================================================================
// Private Methods
//==================================================================================================
private void waitForRunner() {
// let all swing updates finish, which may trigger the update manager
waitForSwing();
while (runner.isBusy()) {
sleep(DEFAULT_WAIT_DELAY);
}
// let any resulting swing events finish
waitForSwing();
}
private class ClientRunnable implements Runnable {
@Override
public void run() {
runnableCalled++;
}
}
}
@@ -25,7 +25,6 @@ import org.junit.Before;
import org.junit.Test;
import generic.test.AbstractGenericTest;
import ghidra.util.Msg;
import ghidra.util.SystemUtilities;
public class SwingUpdateManagerTest extends AbstractGenericTest {
@@ -52,7 +51,7 @@ public class SwingUpdateManagerTest extends AbstractGenericTest {
}
@Test
public void testOneCallFroUpdateNow() {
public void testOneCallFromUpdateNow() {
manager.updateNow();
waitForManager();
assertEquals("Expected only 1 callback", 1, runnableCalled);
@@ -107,6 +106,19 @@ public class SwingUpdateManagerTest extends AbstractGenericTest {
runnableCalled);
}
@Test
public void testFlush() {
manager.flush();
waitForSwing();
assertFalse(manager.isBusy());
assertEquals("Did not expect the callback after stop()", 0, runnableCalled);
manager.updateLater();
manager.flush();
waitForManager();
assertEquals("Expected only 1 callback", 1, runnableCalled);
}
@Test
public void testStop() {
manager.updateLater();
@@ -236,9 +248,26 @@ public class SwingUpdateManagerTest extends AbstractGenericTest {
assertEquals("Expected exactly 2 callbacks", 2, runnableCalled);
}
//==============================================================================================
@Test
public void testNotFiringTooOften() throws InterruptedException {
Thread t = new Thread(() -> {
for (int i = 0; i < 50; i++) {
manager.update();
manager.update();
manager.update();
manager.update();
sleep(10);
}
});
t.start();
t.join();
waitForManager();
assertEquals(2, runnableCalled);
}
//==================================================================================================
// Private Methods
//==============================================================================================
//==================================================================================================
private void waitForManager() {
// let all swing updates finish, which may trigger the update manager
@@ -258,7 +287,6 @@ public class SwingUpdateManagerTest extends AbstractGenericTest {
@Override
public void run() {
runnableCalled++;
Msg.debug(this, "run() called - count: " + runnableCalled);
}
});
}
@@ -1,86 +0,0 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.util.task;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;
import generic.test.AbstractGenericTest;
import ghidra.util.Msg;
import ghidra.util.SystemUtilities;
public class SwingUpdateManagerTimerTest extends AbstractGenericTest {
private static final int MIN_DELAY = 100;
private static final int MAX_DELAY = 10000;
private volatile int runnableCalled;
private SwingUpdateManager manager;
@Before
public void setUp() throws Exception {
manager = createUpdateManager(MIN_DELAY, MAX_DELAY);
// must turn this on to get the expected results, as in headless mode the update manager
// will run it's Swing work immediately on the test thread, which is not true to the
// default behavior
System.setProperty(SystemUtilities.HEADLESS_PROPERTY, Boolean.FALSE.toString());
}
@Test
public void testNotFiringTooOften() throws InterruptedException {
Thread t = new Thread(() -> {
for (int i = 0; i < 50; i++) {
manager.update();
manager.update();
manager.update();
manager.update();
sleep(10);
}
});
t.start();
t.join();
waitForManager();
assertEquals(2, runnableCalled);
}
//==================================================================================================
// Private Methods
//==================================================================================================
private void waitForManager() {
// let all swing updates finish, which may trigger the update manager
waitForSwing();
while (manager.isBusy()) {
sleep(DEFAULT_WAIT_DELAY);
}
// let any resulting swing events finish
waitForSwing();
}
private SwingUpdateManager createUpdateManager(int min, int max) {
return new SwingUpdateManager(min, max, "bob", new Runnable() {
@Override
public void run() {
runnableCalled++;
Msg.debug(this, "run() called - count: " + runnableCalled);
}
});
}
}
@@ -29,8 +29,10 @@ import docking.help.Help;
import docking.help.HelpService;
import docking.widgets.EmptyBorderButton;
import docking.widgets.label.GDLabel;
import ghidra.util.*;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
import ghidra.util.layout.HorizontalLayout;
import ghidra.util.task.BufferedSwingRunner;
import log.LogListener;
import log.LogPanelAppender;
import resources.ResourceManager;
@@ -45,6 +47,8 @@ public class LogPanel extends JPanel implements LogListener {
private JLabel label;
private Color defaultColor;
private BufferedSwingRunner messageUpdater = new BufferedSwingRunner();
LogPanel(final FrontEndPlugin plugin) {
super(new BorderLayout());
JPanel panel = new JPanel(new BorderLayout());
@@ -89,13 +93,12 @@ public class LogPanel extends JPanel implements LogListener {
@Override
public void messageLogged(String message, boolean isError) {
SystemUtilities.runIfSwingOrPostSwingLater(() -> {
label.setForeground(defaultColor);
if (isError) {
label.setForeground(Color.RED);
}
label.setText(message);
label.setToolTipText(message);
messageUpdater.run(() -> {
label.setForeground(isError ? Color.RED : defaultColor);
String text = message.replace("\n", " ");
label.setText(text);
label.setToolTipText(text);
});
}
@@ -115,4 +118,5 @@ public class LogPanel extends JPanel implements LogListener {
logAppender.setLogListener(this);
}
}