mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2024-11-28 23:21:46 +00:00
GP-4100: Fix issues between Debugger and NavHistory plugins.
This commit is contained in:
parent
aec8641320
commit
c3164a1902
@ -37,10 +37,13 @@ public class UnixShellScriptTraceRmiLaunchOffer extends AbstractScriptTraceRmiLa
|
||||
/**
|
||||
* Create a launch offer from the given shell script.
|
||||
*
|
||||
* @param plugin the launcher service plugin
|
||||
* @param program the current program, usually the target image. In general, this should be used
|
||||
* for at least two purposes. 1) To populate the default command line. 2) To ensure
|
||||
* the target image is mapped in the resulting target trace.
|
||||
* @throws FileNotFoundException
|
||||
* @param script the script file that implements this offer
|
||||
* @return the offer
|
||||
* @throws FileNotFoundException if the script file does not exist
|
||||
*/
|
||||
public static UnixShellScriptTraceRmiLaunchOffer create(TraceRmiLauncherServicePlugin plugin,
|
||||
Program program, File script) throws FileNotFoundException {
|
||||
|
@ -225,6 +225,7 @@ public class TraceRmiHandler implements TraceRmiConnection {
|
||||
* <p>
|
||||
* Note it is common for this to be constructed by a TCP <em>client</em>.
|
||||
*
|
||||
* @param plugin the Trace RMI plugin
|
||||
* @param socket the socket to the back-end debugger
|
||||
* @throws IOException if there is an issue with the I/O streams
|
||||
*/
|
||||
|
@ -809,7 +809,7 @@ public class DebuggerBreakpointMarkerPlugin extends Plugin
|
||||
|
||||
BreakpointsDecompilerMarginProvider decompilerMarginProvider;
|
||||
private MarginProviderSupplier functionGraphMarginSupplier =
|
||||
new DefaultMarginProviderSupplier();
|
||||
new DefaultMarginProviderSupplier();
|
||||
|
||||
public DebuggerBreakpointMarkerPlugin(PluginTool tool) {
|
||||
super(tool);
|
||||
@ -973,7 +973,9 @@ public class DebuggerBreakpointMarkerPlugin extends Plugin
|
||||
protected void removeMarkers(Program program) {
|
||||
synchronized (markersByProgram) {
|
||||
BreakpointMarkerSets oldSets = markersByProgram.remove(program);
|
||||
oldSets.dispose();
|
||||
if (oldSets != null) {
|
||||
oldSets.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -434,8 +434,8 @@ public class DebuggerListingProvider extends CodeViewerProvider {
|
||||
* is synchronized exactly with the other providers. "Main" on the other hand, does not
|
||||
* necessarily have that property, but it is still <em>not</em> a snapshot. It is the main
|
||||
* listing presented by this plugin, and so it has certain unique features. Calling
|
||||
* {@link DebuggerListingPlugin#getConnectedProvider()} will return the main dynamic listing,
|
||||
* despite it not really being "connected."
|
||||
* {@link DebuggerListingPlugin#getProvider()} will return the main dynamic listing, despite it
|
||||
* not really being "connected."
|
||||
*
|
||||
* @return true if this is the main listing for the plugin.
|
||||
*/
|
||||
@ -649,8 +649,12 @@ public class DebuggerListingProvider extends CodeViewerProvider {
|
||||
|
||||
@Override
|
||||
protected void doSetProgram(Program newProgram) {
|
||||
// E.g., The "Navigate Previous" could cause a change in trace
|
||||
if (newProgram != null && current.getView() != null && newProgram != current.getView()) {
|
||||
throw new AssertionError();
|
||||
if (!(newProgram instanceof TraceProgramView view)) {
|
||||
throw new IllegalArgumentException("Dynamic Listings require trace views");
|
||||
}
|
||||
traceManager.activateTrace(view.getTrace());
|
||||
}
|
||||
if (getProgram() == newProgram) {
|
||||
return;
|
||||
|
@ -426,6 +426,9 @@ public class DebuggerMemoryBytesProvider extends ProgramByteViewerComponentProvi
|
||||
current = coordinates;
|
||||
addNewListeners();
|
||||
doSetProgram(current.getView());
|
||||
// NB. Also avoid a stale location being reported to the history service.
|
||||
setLocation(null);
|
||||
setSelection(null, false);
|
||||
goToTrait.goToCoordinates(coordinates);
|
||||
trackingTrait.goToCoordinates(coordinates);
|
||||
readsMemTrait.goToCoordinates(coordinates);
|
||||
|
@ -75,7 +75,10 @@ import ghidra.util.task.*;
|
||||
packageName = DebuggerPluginPackage.NAME,
|
||||
status = PluginStatus.RELEASED,
|
||||
eventsProduced = {
|
||||
TraceOpenedPluginEvent.class,
|
||||
TraceActivatedPluginEvent.class,
|
||||
TraceInactiveCoordinatesPluginEvent.class,
|
||||
TraceClosedPluginEvent.class,
|
||||
},
|
||||
eventsConsumed = {
|
||||
TraceActivatedPluginEvent.class,
|
||||
@ -284,6 +287,8 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
|
||||
private DebuggerPlatformService platformService;
|
||||
// @AutoServiceConsumed via method
|
||||
private DebuggerControlService controlService;
|
||||
@AutoServiceConsumed
|
||||
private NavigationHistoryService navigationHistoryService;
|
||||
@SuppressWarnings("unused")
|
||||
private final AutoService.Wiring autoServiceWiring;
|
||||
|
||||
@ -959,6 +964,9 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
|
||||
}
|
||||
|
||||
protected void doTraceClosed(Trace trace) {
|
||||
if (navigationHistoryService != null) {
|
||||
navigationHistoryService.clear(trace.getProgramView());
|
||||
}
|
||||
synchronized (listenersByTrace) {
|
||||
trace.release(this);
|
||||
lastCoordsByTrace.remove(trace);
|
||||
|
@ -348,7 +348,8 @@ public class NavigationHistoryPlugin extends Plugin
|
||||
notifyHistoryChange();
|
||||
}
|
||||
|
||||
private void clear(Program program) {
|
||||
@Override
|
||||
public void clear(Program program) {
|
||||
for (HistoryList historyList : historyListMap.values()) {
|
||||
clear(historyList, program);
|
||||
}
|
||||
@ -657,7 +658,7 @@ public class NavigationHistoryPlugin extends Plugin
|
||||
ProgramLocation location =
|
||||
validateProgramLocation(activeNavigatable, memento.getProgramLocation());
|
||||
if (location != null) {
|
||||
// prefer the active natigatable's location
|
||||
// prefer the active navigatable's location
|
||||
return location;
|
||||
}
|
||||
}
|
||||
@ -683,11 +684,13 @@ public class NavigationHistoryPlugin extends Plugin
|
||||
Program program = location.getProgram();
|
||||
if (program.isClosed()) {
|
||||
|
||||
Msg.showError(this, null, "Closed Program",
|
||||
"The Navigation History Plugin is using a closed program.\n" + "Program: " +
|
||||
program.getName() + "Navigatable: " +
|
||||
navigatable.getClass().getSimpleName() + "\n" + "Location: " +
|
||||
location.getClass().getSimpleName() + " @ " + location.getAddress());
|
||||
Msg.showError(this, null, "Closed Program", """
|
||||
The Navigation History Plugin is using a closed program.
|
||||
Program: %s
|
||||
Navigatable: %s
|
||||
Location: %s @ %s"""
|
||||
.formatted(program.getName(), navigatable.getClass().getSimpleName(),
|
||||
location.getClass().getSimpleName(), location.getAddress()));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -21,31 +21,35 @@ import ghidra.app.nav.LocationMemento;
|
||||
import ghidra.app.nav.Navigatable;
|
||||
import ghidra.app.plugin.core.navigation.NavigationHistoryPlugin;
|
||||
import ghidra.framework.plugintool.ServiceInfo;
|
||||
import ghidra.program.model.listing.Program;
|
||||
|
||||
/**
|
||||
* The ToolStateHistoryService maintains a stack of locations that the user
|
||||
* has visited via a navigation plugin.
|
||||
* It provides methods querying and manipulating this list.
|
||||
* The NavigationHistoryService maintains a stack of locations that the user has visited via a
|
||||
* navigation plugin. It provides methods querying and manipulating this list.
|
||||
*/
|
||||
@ServiceInfo(defaultProvider = NavigationHistoryPlugin.class, description = "Maintains a history of tool states")
|
||||
@ServiceInfo(
|
||||
defaultProvider = NavigationHistoryPlugin.class,
|
||||
description = "Maintains a history of tool states")
|
||||
public interface NavigationHistoryService {
|
||||
|
||||
/**
|
||||
* Positions the current location to the next location in the history list.
|
||||
* If there is no "next" location, the history list remains unchanged.
|
||||
* Positions the current location to the next location in the history list. If there is no
|
||||
* "next" location, the history list remains unchanged.
|
||||
*
|
||||
* @param navigatable the navigatable to be navigated
|
||||
*/
|
||||
public void next(Navigatable navigatable);
|
||||
|
||||
/**
|
||||
* Positions the "current" location to the previous location in the history list.
|
||||
* If there is no "previous" location, the history list remains unchanged.
|
||||
* Positions the "current" location to the previous location in the history list. If there is no
|
||||
* "previous" location, the history list remains unchanged.
|
||||
*
|
||||
* @param navigatable the navigatable to be navigated
|
||||
*/
|
||||
public void previous(Navigatable navigatable);
|
||||
|
||||
/**
|
||||
* Navigates to the given location in the "next" list. If the location is not in the list, then
|
||||
* Navigates to the given location in the "next" list. If the location is not in the list, then
|
||||
* nothing will happen.
|
||||
*
|
||||
* @param navigatable the navigatable to be navigated
|
||||
@ -54,8 +58,8 @@ public interface NavigationHistoryService {
|
||||
public void next(Navigatable navigatable, LocationMemento location);
|
||||
|
||||
/**
|
||||
* Navigates to the given location in the "previous" list. If the location is not in
|
||||
* the list, then nothing will happen
|
||||
* Navigates to the given location in the "previous" list. If the location is not in the list,
|
||||
* then nothing will happen
|
||||
*
|
||||
* @param navigatable the navigatable to be navigated
|
||||
* @param location The location within the "previous" list to which to go.
|
||||
@ -63,23 +67,25 @@ public interface NavigationHistoryService {
|
||||
public void previous(Navigatable navigatable, LocationMemento location);
|
||||
|
||||
/**
|
||||
* Positions the "current" location to the next location which is in a different function
|
||||
* from current one or previous non-code location.
|
||||
* If we are not inside any function, performs like "next".
|
||||
* Positions the "current" location to the next location which is in a different function from
|
||||
* current one or previous non-code location. If we are not inside any function, performs like
|
||||
* "next".
|
||||
*
|
||||
* @param navigatable the navigatable to be navigated
|
||||
*/
|
||||
public void nextFunction(Navigatable navigatable);
|
||||
|
||||
/**
|
||||
* Positions the "previous" location to the next location which is in a different function
|
||||
* from current one or previous non-code location.
|
||||
* If we are not inside any function, performs like "next".
|
||||
* Positions the "previous" location to the next location which is in a different function from
|
||||
* current one or previous non-code location. If we are not inside any function, performs like
|
||||
* "next".
|
||||
*
|
||||
* @param navigatable the navigatable to be navigated
|
||||
*/
|
||||
public void previousFunction(Navigatable navigatable);
|
||||
|
||||
/**
|
||||
* Returns the LocationMemento objects in the "previous" list
|
||||
* Returns the {@link LocationMemento} objects in the "previous" list
|
||||
*
|
||||
* @param navigatable the navigatable to be navigated
|
||||
* @return the LocationMemento objects in the "previous" list
|
||||
@ -87,7 +93,7 @@ public interface NavigationHistoryService {
|
||||
public List<LocationMemento> getPreviousLocations(Navigatable navigatable);
|
||||
|
||||
/**
|
||||
* Returns the LocationMemento objects in the "next" list
|
||||
* Returns the {@link LocationMemento} objects in the "next" list
|
||||
*
|
||||
* @param navigatable the navigatable to be navigated
|
||||
* @return the LocationMemento objects in the "next" list
|
||||
@ -112,6 +118,7 @@ public interface NavigationHistoryService {
|
||||
|
||||
/**
|
||||
* Returns true if there is a valid "next" function location in the history list
|
||||
*
|
||||
* @param navigatable Navigatable object we are looking at
|
||||
* @return true if there is a valid "next" function location
|
||||
*/
|
||||
@ -119,23 +126,31 @@ public interface NavigationHistoryService {
|
||||
|
||||
/**
|
||||
* Returns true if there is a valid "previous" function location in the history list
|
||||
*
|
||||
* @param navigatable Navigatable object we are looking at
|
||||
* @return true if there is a valid "previous" function location
|
||||
*/
|
||||
public boolean hasPreviousFunction(Navigatable navigatable);
|
||||
|
||||
/**
|
||||
* Adds the given locationMomento to the list of previous locations. Clears the list
|
||||
* of next locations.
|
||||
* Adds the current location memento to the list of previous locations for the given
|
||||
* navigatable. Clears the list of next locations.
|
||||
*
|
||||
* @param navigatable the navigatable to be navigated
|
||||
*/
|
||||
public void addNewLocation(Navigatable navigatable);
|
||||
|
||||
/**
|
||||
* Removes all visited locations from the history list
|
||||
* Removes all visited locations from the history list for the given navigatable
|
||||
*
|
||||
* @param navigatable the navigatable to be navigated
|
||||
* @param navigatable the navigatable whose list to be cleared
|
||||
*/
|
||||
public void clear(Navigatable navigatable);
|
||||
|
||||
/**
|
||||
* Removes all entries for the given program from all history lists
|
||||
*
|
||||
* @param program the program whose entries to be cleared
|
||||
*/
|
||||
public void clear(Program program);
|
||||
}
|
||||
|
@ -170,7 +170,7 @@ public class ProgramByteViewerComponentProvider extends ByteViewerComponentProvi
|
||||
return getCurrentTextSelection();
|
||||
}
|
||||
|
||||
private void setSelection(ProgramSelection selection, boolean notify) {
|
||||
protected void setSelection(ProgramSelection selection, boolean notify) {
|
||||
currentSelection = selection;
|
||||
if (selection == null) {
|
||||
return;
|
||||
|
@ -16,11 +16,15 @@
|
||||
package docking;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import javax.swing.JMenuBar;
|
||||
|
||||
import docking.action.DockingActionIf;
|
||||
import docking.menu.*;
|
||||
import generic.concurrent.ReentryGuard;
|
||||
import generic.concurrent.ReentryGuard.Guarded;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
public class WindowActionManager {
|
||||
private Map<DockingActionIf, DockingActionProxy> actionToProxyMap;
|
||||
@ -30,6 +34,24 @@ public class WindowActionManager {
|
||||
|
||||
private boolean disposed;
|
||||
|
||||
/**
|
||||
* Some actions' {@link DockingActionIf#isEnabledForContext(ActionContext)} methods may
|
||||
* inadvertently display an error dialog, allowing for the Swing thread to reenter and modify
|
||||
* the {@link #actionToProxyMap}. We want to allow this, but detect it and bail early.
|
||||
*/
|
||||
private ReentryGuard<Throwable> reentryGuard = new ReentryGuard<>() {
|
||||
@Override
|
||||
protected Throwable violated(boolean nested, Throwable previous) {
|
||||
if (previous != null) {
|
||||
return previous;
|
||||
}
|
||||
Throwable t = new Throwable();
|
||||
Msg.error(WindowActionManager.this,
|
||||
"Modified action list during context change update", t);
|
||||
return t;
|
||||
}
|
||||
};
|
||||
|
||||
WindowActionManager(WindowNode node, MenuHandler menuBarHandler,
|
||||
DockingWindowManager winMgr, MenuGroupMap menuGroupMap) {
|
||||
|
||||
@ -40,6 +62,7 @@ public class WindowActionManager {
|
||||
}
|
||||
|
||||
void setActions(List<DockingActionIf> actionList) {
|
||||
reentryGuard.checkAccess();
|
||||
menuBarMgr.clearActions();
|
||||
toolBarMgr.clearActions();
|
||||
actionToProxyMap.clear();
|
||||
@ -49,6 +72,7 @@ public class WindowActionManager {
|
||||
}
|
||||
|
||||
void addAction(DockingActionIf action) {
|
||||
reentryGuard.checkAccess();
|
||||
if (action.getMenuBarData() != null || action.getToolBarData() != null) {
|
||||
DockingActionProxy proxyAction = new DockingActionProxy(action);
|
||||
actionToProxyMap.put(action, proxyAction);
|
||||
@ -58,6 +82,7 @@ public class WindowActionManager {
|
||||
}
|
||||
|
||||
void removeAction(DockingActionIf action) {
|
||||
reentryGuard.checkAccess();
|
||||
DockingActionProxy proxyAction = actionToProxyMap.remove(action);
|
||||
if (proxyAction != null) {
|
||||
menuBarMgr.removeAction(proxyAction);
|
||||
@ -80,6 +105,7 @@ public class WindowActionManager {
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
reentryGuard.checkAccess();
|
||||
disposed = true;
|
||||
node.setToolBar(null);
|
||||
node.setMenuBar(null);
|
||||
@ -90,28 +116,42 @@ public class WindowActionManager {
|
||||
|
||||
void contextChanged(Map<Class<? extends ActionContext>, ActionContext> defaultContextMap,
|
||||
ActionContext localContext, Set<DockingActionIf> excluded) {
|
||||
|
||||
if (!node.isVisible() || disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update actions - make a copy so that we don't get concurrent modification
|
||||
// exceptions during reentrant operations
|
||||
List<DockingActionIf> list = new ArrayList<>(actionToProxyMap.keySet());
|
||||
for (DockingActionIf action : list) {
|
||||
if (excluded.contains(action)) {
|
||||
continue;
|
||||
/**
|
||||
* We need the guard against reentrant changes to the actionToProxyMap, lest the iterator
|
||||
* throw a ConcurrentModificationException. If the guard finds a violation, i.e., the map
|
||||
* has changed, we just bail. Whatever changed the map should also trigger an update to
|
||||
* context, and so we should have a fresh go here with the new map.
|
||||
*
|
||||
* There are three points where there could be reentrant modifications. Those are 1) when
|
||||
* computing an action's context, 2) when checking if the action is enabled for that
|
||||
* context, and 3) when setting the proxy's enabled state. There are minimal performance
|
||||
* trade-offs to consider when deciding which points to check. Critically, we must check
|
||||
* somewhere between the last point and the end of the loop, i.e., before stepping the
|
||||
* iterator. We opt to check only that last point, since reentry is uncommon here.
|
||||
*/
|
||||
try (Guarded guarded = reentryGuard.enter()) {
|
||||
if (!node.isVisible() || disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
DockingActionIf proxyAction = actionToProxyMap.get(action);
|
||||
ActionContext context =
|
||||
getContextForAction(action, localContext, defaultContextMap);
|
||||
for (Entry<DockingActionIf, DockingActionProxy> ent : actionToProxyMap.entrySet()) {
|
||||
DockingActionIf action = ent.getKey();
|
||||
DockingActionIf proxyAction = ent.getValue();
|
||||
if (excluded.contains(action)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (context != null) {
|
||||
proxyAction.setEnabled(proxyAction.isEnabledForContext(context));
|
||||
}
|
||||
else {
|
||||
proxyAction.setEnabled(false);
|
||||
// Reentry point 1
|
||||
ActionContext context =
|
||||
getContextForAction(action, localContext, defaultContextMap);
|
||||
// Reentry point 2
|
||||
boolean enabled =
|
||||
context == null ? false : proxyAction.isEnabledForContext(context);
|
||||
// Reentry point 3, which we check
|
||||
proxyAction.setEnabled(enabled);
|
||||
if (reentryGuard.isViolated()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -134,7 +174,8 @@ public class WindowActionManager {
|
||||
/**
|
||||
* Returns the set of actions for this window.
|
||||
*
|
||||
* <p>Note this returns the the original passed-in actions and not the proxy actions that the
|
||||
* <p>
|
||||
* Note this returns the the original passed-in actions and not the proxy actions that the
|
||||
* window uses.
|
||||
*
|
||||
* @return the set of actions for this window
|
||||
|
@ -0,0 +1,185 @@
|
||||
/* ###
|
||||
* 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 generic.concurrent;
|
||||
|
||||
/**
|
||||
* A means of detecting and handling reentrant conditions.
|
||||
*
|
||||
* <p>
|
||||
* One example where this has been applied deals with updating actions upon changes in context. If,
|
||||
* in the course of determining which actions are enabled, one of the {@code isEnabled} methods
|
||||
* displays an error dialog, the Swing thread reenters its main loop while that dialog is showing,
|
||||
* but before {@code isEnabled} has returned. This can cause all sorts of unexpected behaviors.
|
||||
* Namely, a timer could fire, context could change again, etc., and the list of actions being
|
||||
* updated may also change. At worst, this could result in many exceptions being thrown, because a
|
||||
* data structure has been modified concurrently. At best, if the loop is allowed to finish, there's
|
||||
* a lot of wasted time updating actions that will never be displayed.
|
||||
*
|
||||
* <p>
|
||||
* In that example, the loop that updates the actions would be the "guarded block." Any point at
|
||||
* which the list of actions is modified might result in "reentrant access" and should be checked.
|
||||
*
|
||||
* <p>
|
||||
* This class provides a primitive for instrumenting, detecting, and properly reacting to such
|
||||
* conditions. For example, if the modification should not be allowed at all, the guard can throw an
|
||||
* exception at the moment of reentrant access. Alternatively, if the modification should be
|
||||
* allowed, the guard would simply set a flag, then the guarded block can check that flag and
|
||||
* terminate early.
|
||||
*
|
||||
* <p>
|
||||
* This implementation is <em>not</em> thread safe. It is designed to check for reentrant access,
|
||||
* not concurrent access. The client must ensure that only one thread enters the guarded block or
|
||||
* calls {@link #checkAccess()} at a time. Otherwise, the behavior is undefined.
|
||||
*
|
||||
* <pre>
|
||||
* public class ActionManager {
|
||||
* private final ReentryGuard<Throwable> reentryGuard = new ReentryGuard<>() {
|
||||
* @Override
|
||||
* public Throwable violated(boolean nested, Throwable previous) {
|
||||
* if (previous != null) {
|
||||
* return previous;
|
||||
* }
|
||||
* return new Throwable(); // record the stack of the violation
|
||||
* }
|
||||
* };
|
||||
* private final List<Action> actions;
|
||||
*
|
||||
* public void addAction(Action action) {
|
||||
* // Notify the guard we've committed some reentrant behavior.
|
||||
* // Would need to add this to anything that modifies the action list.
|
||||
* reentryGuard.checkAccess();
|
||||
* actions.add(action);
|
||||
* }
|
||||
*
|
||||
* public void updateActions(Context ctx) {
|
||||
* try (Guarded guarded = reentryGuard.enter()) {
|
||||
* // There is no need to create a copy, since we'll bail before the next iteration
|
||||
* for (Action action : actions) {
|
||||
* boolean enabled = action.isEnabledForContext(ctx);
|
||||
* if (reentryGuard.getViolation() != null) {
|
||||
* break; // Actions has been modified. Bail.
|
||||
* // NOTE: This leaves the update incomplete.
|
||||
* // Something has to call updateActions again.
|
||||
* }
|
||||
* actions.setEnabled(enabled);
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @param <T> the type used to record information about a violation. It cannot be {@link Void}.
|
||||
*/
|
||||
public abstract class ReentryGuard<T> {
|
||||
private final Guarded guarded = new Guarded(this);
|
||||
private boolean inGuarded;
|
||||
private T violation;
|
||||
|
||||
public static class Guarded implements AutoCloseable {
|
||||
private ReentryGuard<?> guard;
|
||||
|
||||
public Guarded(ReentryGuard<?> guard) {
|
||||
this.guard = guard;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
guard.inGuarded = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the guard of entry into the guarded block
|
||||
*
|
||||
* <p>
|
||||
* This should always be used in a {@code try-with-resources} block. This will ensure that the
|
||||
* guard is notified of exit from the guarded block, even in exceptional circumstances.
|
||||
*
|
||||
* <p>
|
||||
* NOTE: Re-entering the guarded portion is itself a violation.
|
||||
*
|
||||
* @return a closeable for notifying the guard of exit from the guarded block, or null if
|
||||
* reentering the guarded block
|
||||
*/
|
||||
public Guarded enter() {
|
||||
if (inGuarded) {
|
||||
violation = violated(true, violation);
|
||||
return null;
|
||||
}
|
||||
inGuarded = true;
|
||||
violation = null;
|
||||
return guarded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the guard of access to some resource used by the guarded block
|
||||
*
|
||||
* <p>
|
||||
* If the access turns out to be reentrant, i.e., the thread's current stack includes a frame in
|
||||
* the guarded block, this will call {@link #violated(boolean, Object)} and record the result.
|
||||
* It can be inspected later via {@link #getViolation()}.
|
||||
*/
|
||||
public void checkAccess() {
|
||||
if (inGuarded) {
|
||||
violation = violated(false, violation);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a violation
|
||||
*
|
||||
* <p>
|
||||
* This method is called if {@link #checkAccess()} or {@link #enter()} is called while already
|
||||
* inside a guarded block. Its return value is stored for later inspection by
|
||||
* {@link #getViolation()}. It's possible multiple violations occur within one execution of the
|
||||
* guarded block. The previous return value of this method is provided, if that is the case. To
|
||||
* record only the first violation, this method should just {@code return previous} when it is
|
||||
* non-null. To record only the last violation, this method should disregard {@code previous}.
|
||||
* To record all violations, {@code T} will need to be a collection, and this method will need
|
||||
* to create and/or append to the collection.
|
||||
*
|
||||
* @param nested true if the violation is a nested call to {@link #enter()}; false if the
|
||||
* violation is a call to {@link #checkAccess()}
|
||||
* @param previous the previous return value of this method, on the occasion of multiple
|
||||
* violations
|
||||
* @return the record of the violation
|
||||
*/
|
||||
protected abstract T violated(boolean nested, T previous);
|
||||
|
||||
/**
|
||||
* Retrieve a violation, if applicable
|
||||
*
|
||||
* <p>
|
||||
* Calling this method outside of a guarded block has undefined behavior.
|
||||
*
|
||||
* @return the violation; or null to indicate no violation
|
||||
*/
|
||||
public T getViolation() {
|
||||
return violation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is a violation
|
||||
*
|
||||
* <p>
|
||||
* This is equivalent to checking if {@link #getViolation()} returns non-null.
|
||||
*
|
||||
* @return true if there is a violation.
|
||||
*/
|
||||
public boolean isViolated() {
|
||||
return violation != null;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user