diff --git a/Ghidra/Framework/Generic/src/main/java/generic/test/AbstractGenericTest.java b/Ghidra/Framework/Generic/src/main/java/generic/test/AbstractGenericTest.java index 3552574e71..e069eabb60 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/test/AbstractGenericTest.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/test/AbstractGenericTest.java @@ -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 set = new HashSet<>(); + Set set = new HashSet<>(); runSwing(() -> { @SuppressWarnings("unchecked") WeakSet s = (WeakSet) 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 managers, boolean flush) { + private static boolean waitForSwing(Set 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 managers, boolean flush) { + private static void flushAllManagers(Set 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(); } diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/AbstractSwingUpdateManager.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/AbstractSwingUpdateManager.java new file mode 100644 index 0000000000..89ce77ffde --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/AbstractSwingUpdateManager.java @@ -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. + *

+ * The various methods dictate when the client will get a callback:

+ *

    + *
  • {@link #update()} - if this is the first call to update, then do the work + * immediately; otherwise, buffer the update request until the + * timeout has expired.
  • + *
  • {@link #updateNow()} - perform the callback now.
  • + *
  • {@link #updateLater()} - buffer the update request until the timeout has expired.
  • + *
  • 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: + *
    {@literal
    + *                          	SwingUtilities.invokeLater(() -> updateManager.updateNow());
    + *                          }
    + *
  • + *
+ * + *

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 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 + *

+ * Note: The minDelay 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 + *

+ * Note: The minDelay 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 + *

+ * Note: The minDelay 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; + } + +} diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/BufferedSwingRunner.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/BufferedSwingRunner.java new file mode 100644 index 0000000000..00bb18009d --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/BufferedSwingRunner.java @@ -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 + *

+ * Note: The minDelay 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 run, then do the work + * immediately; otherwise, buffer the request until the timeout has expired. + * + *

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. + * + *

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); + } +} diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/SwingUpdateManager.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/SwingUpdateManager.java index 9e5d113777..9615241bde 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/SwingUpdateManager.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/SwingUpdateManager.java @@ -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 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 *

- * Note: The minDelay will always be at least {@link #MIN_DELAY_FLOOR}, regardless of - * the given value. + * Note: The minDelay 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 *

- * Note: The minDelay will always be at least {@link #MIN_DELAY_FLOOR}, regardless of - * the given value. + * Note: The minDelay 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); } } diff --git a/Ghidra/Framework/Generic/src/main/java/log/LogPanelAppender.java b/Ghidra/Framework/Generic/src/main/java/log/LogPanelAppender.java index c8e7ddf592..c3df205c70 100644 --- a/Ghidra/Framework/Generic/src/main/java/log/LogPanelAppender.java +++ b/Ghidra/Framework/Generic/src/main/java/log/LogPanelAppender.java @@ -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 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 diff --git a/Ghidra/Framework/Generic/src/test/java/ghidra/util/task/BufferedSwingRunnerTest.java b/Ghidra/Framework/Generic/src/test/java/ghidra/util/task/BufferedSwingRunnerTest.java new file mode 100644 index 0000000000..33ad77fd89 --- /dev/null +++ b/Ghidra/Framework/Generic/src/test/java/ghidra/util/task/BufferedSwingRunnerTest.java @@ -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++; + } + } +} diff --git a/Ghidra/Framework/Generic/src/test/java/ghidra/util/task/SwingUpdateManagerTest.java b/Ghidra/Framework/Generic/src/test/java/ghidra/util/task/SwingUpdateManagerTest.java index d1ea338841..c14664c7cc 100644 --- a/Ghidra/Framework/Generic/src/test/java/ghidra/util/task/SwingUpdateManagerTest.java +++ b/Ghidra/Framework/Generic/src/test/java/ghidra/util/task/SwingUpdateManagerTest.java @@ -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); } }); } diff --git a/Ghidra/Framework/Generic/src/test/java/ghidra/util/task/SwingUpdateManagerTimerTest.java b/Ghidra/Framework/Generic/src/test/java/ghidra/util/task/SwingUpdateManagerTimerTest.java deleted file mode 100644 index 473cefbfe0..0000000000 --- a/Ghidra/Framework/Generic/src/test/java/ghidra/util/task/SwingUpdateManagerTimerTest.java +++ /dev/null @@ -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); - } - }); - } -} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/LogPanel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/LogPanel.java index 5c880bedb0..2a74043fbc 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/LogPanel.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/LogPanel.java @@ -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); } + }