GP-1608: DebuggerListing use GTabPanel. No tabs in Threads.

This commit is contained in:
Dan 2024-04-03 16:02:06 -04:00
parent 1fa19633d3
commit 04d2e88c2d
26 changed files with 579 additions and 653 deletions

View file

@ -135,6 +135,13 @@ public interface Target {
}
}
/**
* Describe the target for display in the UI
*
* @return the description
*/
String describe();
/**
* Check if the target is still valid
*

View file

@ -45,6 +45,16 @@ import ghidra.util.NotOwnerException;
public class DebuggerCoordinates {
/**
* Coordinates that indicate no trace is active in the Debugger UI.
*
* <p>
* Typically, that only happens when no trace is open. Telling the trace manager to activate
* {@code NOWHERE} will cause it to instead activate the most recently active trace, which may
* very well be the current trace, resulting in no change. Internally, the trace manager will
* activate {@code NOWHERE} whenever the current trace is closed, effectively activating the
* most recent trace other than the one just closed.
*/
public static final DebuggerCoordinates NOWHERE =
new DebuggerCoordinates(null, null, null, null, null, null, null, null);

View file

@ -81,6 +81,12 @@ public class TraceRmiTarget extends AbstractTarget {
this.supportedBreakpointKinds = computeSupportedBreakpointKinds();
}
@Override
public String describe() {
return "%s in %s at %s (rmi)".formatted(getTrace().getDomainFile().getName(),
connection.getDescription(), connection.getRemoteAddress());
}
@Override
public boolean isValid() {
return !connection.isClosed() && connection.isTarget(trace);

View file

@ -1614,7 +1614,7 @@ public interface DebuggerResources {
}
interface CloseAllTracesAction extends CloseTraceAction {
String NAME = NAME_PREFIX + " All Traces";
String NAME = NAME_PREFIX + "All Traces";
String DESCRIPTION = "Close all traces";
String HELP_ANCHOR = "close_all_traces";
@ -1641,7 +1641,7 @@ public interface DebuggerResources {
}
interface CloseOtherTracesAction extends CloseTraceAction {
String NAME = NAME_PREFIX + " Other Traces";
String NAME = NAME_PREFIX + "Other Traces";
String DESCRIPTION = "Close all traces except the current one";
String HELP_ANCHOR = "close_other_traces";
@ -1668,7 +1668,7 @@ public interface DebuggerResources {
}
interface CloseDeadTracesAction extends CloseTraceAction {
String NAME = NAME_PREFIX + " Dead Traces";
String NAME = NAME_PREFIX + "Dead Traces";
String DESCRIPTION = "Close all traces not being recorded";
String HELP_ANCHOR = "close_dead_traces";

View file

@ -55,9 +55,6 @@ import ghidra.trace.model.time.schedule.TraceSchedule;
import ghidra.trace.util.TraceEvents;
import ghidra.util.Msg;
import ghidra.util.Swing;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.Task;
import ghidra.util.task.TaskMonitor;
@PluginInfo(
shortDescription = "Debugger global controls",
@ -357,18 +354,7 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
if (target == null) {
return;
}
TargetActionTask.executeTask(tool, new Task("Disconnect", false, false, false) {
@Override
public void run(TaskMonitor monitor) throws CancelledException {
try {
target.disconnect();
}
catch (Exception e) {
tool.setStatusInfo("Disconnect failed: " + e, true);
Msg.error(this, "Disconnect failed: " + e, e);
}
}
});
TargetActionTask.executeTask(tool, new DisconnectTask(tool, List.of(target)));
}
private boolean haveEmuAndTrace() {

View file

@ -0,0 +1,53 @@
/* ###
* 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.app.plugin.core.debug.gui.control;
import java.util.Collection;
import java.util.List;
import ghidra.debug.api.target.Target;
import ghidra.framework.plugintool.PluginTool;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.Task;
import ghidra.util.task.TaskMonitor;
public class DisconnectTask extends Task {
private final PluginTool tool;
private final List<Target> targets;
public DisconnectTask(PluginTool tool, Collection<Target> targets) {
super("Disconnect", false, true, false);
this.tool = tool;
this.targets = List.copyOf(targets);
}
@Override
public void run(TaskMonitor monitor) throws CancelledException {
monitor.initialize(targets.size(), "Disconnecting...");
for (Target target : targets) {
try {
monitor.setMessage("Disconnecting " + target.describe());
target.disconnect();
monitor.increment();
}
catch (Exception e) {
tool.setStatusInfo("Disconnect failed: " + e, true);
Msg.error(this, "Disconnect failed: " + e, e);
}
}
}
}

View file

@ -57,6 +57,8 @@ import ghidra.app.plugin.core.debug.gui.DebuggerResources.FollowsCurrentThreadAc
import ghidra.app.plugin.core.debug.gui.DebuggerResources.OpenProgramAction;
import ghidra.app.plugin.core.debug.gui.action.*;
import ghidra.app.plugin.core.debug.gui.modules.DebuggerMissingModuleActionContext;
import ghidra.app.plugin.core.debug.gui.thread.DebuggerTraceFileActionContext;
import ghidra.app.plugin.core.debug.gui.trace.DebuggerTraceTabPanel;
import ghidra.app.plugin.core.debug.utils.ProgramLocationUtils;
import ghidra.app.plugin.core.debug.utils.ProgramURLUtils;
import ghidra.app.plugin.core.marker.MarkerMarginProvider;
@ -342,6 +344,7 @@ public class DebuggerListingProvider extends CodeViewerProvider {
protected final ListenerSet<LocationTrackingSpecChangeListener> trackingSpecChangeListeners =
new ListenerSet<>(LocationTrackingSpecChangeListener.class, true);
protected final DebuggerTraceTabPanel traceTabs;
protected final DebuggerLocationLabel locationLabel = new DebuggerLocationLabel();
protected final JLabel trackingLabel = new JLabel();
@ -372,6 +375,8 @@ public class DebuggerListingProvider extends CodeViewerProvider {
this.plugin = plugin;
this.isMainListing = isConnected;
// TODO: An icon to distinguish dynamic from static
syncTrait = new ForListingSyncTrait();
goToTrait = new ForListingGoToTrait();
trackingTrait = new ForListingTrackingTrait();
@ -394,13 +399,21 @@ public class DebuggerListingProvider extends CodeViewerProvider {
readsMemTrait.goToCoordinates(current);
locationLabel.goToCoordinates(current);
// TODO: An icon to distinguish dynamic from static
if (isConnected) {
traceTabs = new DebuggerTraceTabPanel(plugin);
}
else {
traceTabs = null;
}
addDisplayListener(readsMemTrait.getDisplayListener());
JPanel northPanel = new JPanel(new BorderLayout());
northPanel.add(locationLabel);
northPanel.add(trackingLabel, BorderLayout.EAST);
if (traceTabs != null) {
northPanel.add(traceTabs, BorderLayout.NORTH);
}
this.setNorthComponent(northPanel);
if (isConnected) {
setTitle(DebuggerResources.TITLE_PROVIDER_LISTING);
@ -929,6 +942,12 @@ public class DebuggerListingProvider extends CodeViewerProvider {
@Override
public ActionContext getActionContext(MouseEvent event) {
if (traceTabs != null) {
DebuggerTraceFileActionContext traceCtx = traceTabs.getActionContext(event);
if (traceCtx != null) {
return traceCtx;
}
}
if (event == null || event.getSource() != locationLabel) {
return super.getActionContext(event);
}
@ -1036,7 +1055,6 @@ public class DebuggerListingProvider extends CodeViewerProvider {
.collect(Collectors.toSet());
// Attempt to open probable matches. All others, list to import
// TODO: What if sections are not presented?
for (TraceModule mod : modules) {
DomainFile match = mappingService.findBestModuleProgram(space, mod);
if (match == null) {
@ -1064,8 +1082,9 @@ public class DebuggerListingProvider extends CodeViewerProvider {
new DebuggerMissingModuleActionContext(mod));
}
/**
* Once the programs are opened, including those which are successfully imported, the mapper
* bot should take over, eventually invoking callbacks to our mapping change listener.
* Once the programs are opened, including those which are successfully imported, the
* automatic mapper should take effect, eventually invoking callbacks to our mapping change
* listener.
*/
}

View file

@ -77,6 +77,14 @@ public class DebuggerThreadsPanel extends AbstractObjectsTableBasedPanel<TraceOb
coords);
}
DebuggerCoordinates coordsForObject(TraceObject object) {
if (provider.current.getTrace() != object.getTrace()) {
// This can happen transiently, so just find something graceful
return DebuggerCoordinates.NOWHERE.object(object);
}
return provider.current.object(object);
}
private class ThreadPcColumn extends TraceValueObjectPropertyColumn<Address> {
public ThreadPcColumn() {
super(Address.class);
@ -85,7 +93,8 @@ public class DebuggerThreadsPanel extends AbstractObjectsTableBasedPanel<TraceOb
@Override
public ValueProperty<Address> getProperty(ValueRow row) {
TraceObject obj = row.getValue().getChild();
DebuggerCoordinates coords = provider.current.object(obj);
DebuggerCoordinates coords = coordsForObject(obj);
return new ValueAddressProperty(row) {
@Override
public Address getValue() {
@ -111,7 +120,7 @@ public class DebuggerThreadsPanel extends AbstractObjectsTableBasedPanel<TraceOb
public Function getValue(ValueRow rowObject, Settings settings, Trace data,
ServiceProvider serviceProvider) throws IllegalArgumentException {
TraceObject obj = rowObject.getValue().getChild();
DebuggerCoordinates coords = provider.current.object(obj);
DebuggerCoordinates coords = coordsForObject(obj);
Address pc = computeProgramCounter(coords);
if (pc == null) {
return null;
@ -130,7 +139,7 @@ public class DebuggerThreadsPanel extends AbstractObjectsTableBasedPanel<TraceOb
public String getValue(ValueRow rowObject, Settings settings, Trace data,
ServiceProvider serviceProvider) throws IllegalArgumentException {
TraceObject obj = rowObject.getValue().getChild();
DebuggerCoordinates coords = provider.current.object(obj);
DebuggerCoordinates coords = coordsForObject(obj);
Address pc = computeProgramCounter(coords);
if (pc == null) {
return null;
@ -147,7 +156,7 @@ public class DebuggerThreadsPanel extends AbstractObjectsTableBasedPanel<TraceOb
@Override
public ValueProperty<Address> getProperty(ValueRow row) {
TraceObject obj = row.getValue().getChild();
DebuggerCoordinates coords = provider.current.object(obj);
DebuggerCoordinates coords = coordsForObject(obj);
return new ValueAddressProperty(row) {
@Override
public Address getValue() {

View file

@ -28,7 +28,8 @@ import docking.WindowPosition;
import docking.action.DockingActionIf;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.services.*;
import ghidra.app.services.DebuggerEmulationService;
import ghidra.app.services.DebuggerTargetService;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.model.DomainObjectChangeRecord;
import ghidra.framework.model.DomainObjectEvent;
@ -97,8 +98,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
@AutoServiceConsumed
DebuggerTargetService targetService;
// @AutoServiceConsumed // via method
private DebuggerTraceManagerService traceManager;
@SuppressWarnings("unused")
private final AutoService.Wiring autoServiceWiring;
@ -106,7 +105,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
private JPanel mainPanel;
DebuggerTraceTabPanel traceTabs;
JPopupMenu traceTabPopupMenu;
DebuggerThreadsPanel panel;
DebuggerLegacyThreadsPanel legacyPanel;
@ -133,12 +131,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
setVisible(true);
}
@AutoServiceConsumed
public void setTraceManager(DebuggerTraceManagerService traceManager) {
this.traceManager = traceManager;
contextChanged();
}
@AutoServiceConsumed
public void setEmulationService(DebuggerEmulationService emulationService) {
contextChanged();
@ -152,7 +144,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
current = coordinates;
traceTabs.coordinatesActivated(coordinates);
if (Trace.isLegacy(coordinates.getTrace())) {
panel.coordinatesActivated(DebuggerCoordinates.NOWHERE);
legacyPanel.coordinatesActivated(coordinates);
@ -191,10 +182,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
myActionContext = legacyPanel.getActionContext();
}
void traceTabsContextChanged() {
myActionContext = traceTabs.getActionContext();
}
@Override
public ActionContext getActionContext(MouseEvent event) {
if (myActionContext == null) {
@ -211,10 +198,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
panel = new DebuggerThreadsPanel(this);
legacyPanel = new DebuggerLegacyThreadsPanel(plugin, this);
mainPanel.add(panel);
traceTabs = new DebuggerTraceTabPanel(this);
mainPanel.add(traceTabs, BorderLayout.NORTH);
}
protected void createActions() {

View file

@ -13,27 +13,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.thread;
package ghidra.app.plugin.core.debug.gui.trace;
import java.awt.Rectangle;
import java.awt.event.*;
import java.util.Objects;
import java.awt.event.MouseEvent;
import javax.swing.Icon;
import javax.swing.JList;
import javax.swing.event.ListSelectionEvent;
import docking.action.DockingAction;
import docking.widgets.HorizontalTabPanel;
import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent;
import ghidra.app.plugin.core.debug.event.TraceOpenedPluginEvent;
import docking.widgets.tab.GTabPanel;
import ghidra.app.plugin.core.debug.event.*;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.*;
import ghidra.app.plugin.core.debug.gui.thread.DebuggerTraceFileActionContext;
import ghidra.app.plugin.core.progmgr.MultiTabPlugin;
import ghidra.app.services.DebuggerTargetService;
import ghidra.app.services.DebuggerTraceManagerService;
import ghidra.debug.api.target.Target;
import ghidra.debug.api.target.TargetPublicationListener;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.model.*;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
import ghidra.framework.plugintool.util.PluginEventListener;
@ -42,26 +39,21 @@ import ghidra.util.Swing;
import utilities.util.SuppressableCallback;
import utilities.util.SuppressableCallback.Suppression;
public class DebuggerTraceTabPanel extends HorizontalTabPanel<Trace>
implements PluginEventListener {
public class DebuggerTraceTabPanel extends GTabPanel<Trace>
implements PluginEventListener, DomainObjectListener {
private class TargetsChangeListener implements TargetPublicationListener {
@Override
public void targetPublished(Target target) {
Swing.runIfSwingOrRunLater(() -> repaint());
Swing.runIfSwingOrRunLater(() -> refreshTab(target.getTrace()));
}
@Override
public void targetWithdrawn(Target target) {
Swing.runIfSwingOrRunLater(() -> repaint());
Swing.runIfSwingOrRunLater(() -> refreshTab(target.getTrace()));
}
}
private final DebuggerThreadsPlugin plugin;
private final DebuggerThreadsProvider provider;
// @AutoServiceConsumed by method
DebuggerTargetService targetService;
@AutoServiceConsumed
@ -78,101 +70,80 @@ public class DebuggerTraceTabPanel extends HorizontalTabPanel<Trace>
private final SuppressableCallback<Void> cbCoordinateActivation = new SuppressableCallback<>();
private DebuggerTraceFileActionContext myActionContext;
public DebuggerTraceTabPanel(DebuggerThreadsProvider provider) {
this.plugin = provider.plugin;
this.provider = provider;
public DebuggerTraceTabPanel(Plugin plugin) {
super("Trace");
this.autoServiceWiring = AutoService.wireServicesConsumed(plugin, this);
PluginTool tool = plugin.getTool();
tool.addEventListener(TraceOpenedPluginEvent.class, this);
tool.addEventListener(TraceActivatedPluginEvent.class, this);
tool.addEventListener(TraceClosedPluginEvent.class, this);
list.setCellRenderer(new TabListCellRenderer<>() {
protected String getText(Trace value) {
return value.getName();
}
protected Icon getIcon(Trace value) {
if (targetService == null) {
return super.getIcon(value);
}
Target target = targetService.getTarget(value);
if (target == null || !target.isValid()) {
return super.getIcon(value);
}
return DebuggerResources.ICON_RECORD;
}
});
list.getSelectionModel().addListSelectionListener(this::traceTabSelected);
list.addFocusListener(new FocusAdapter() {
@Override
public void focusGained(FocusEvent e) {
setTraceTabActionContext(null);
}
});
list.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
setTraceTabActionContext(e);
}
});
setNameFunction(this::getNameForTrace);
setIconFunction(this::getIconForTrace);
setToolTipFunction(this::getTipForTrace);
setSelectedTabConsumer(this::traceTabSelected);
// Cannot use method ref here, since traceManager is still null
setCloseTabConsumer(t -> traceManager.closeTrace(t));
actionCloseTrace = CloseTraceAction.builderPopup(plugin)
.withContext(DebuggerTraceFileActionContext.class)
.popupWhen(c -> c.getTrace() != null)
.popupWhen(c -> {
Trace trace = c.getTrace();
if (trace == null) {
return false;
}
actionCloseTrace.getPopupMenuData()
.setMenuItemName(CloseTraceAction.NAME_PREFIX + getNameForTrace(trace));
return true;
})
.onAction(c -> traceManager.closeTrace(c.getTrace()))
.buildAndInstallLocal(provider);
.buildAndInstall(tool);
actionCloseAllTraces = CloseAllTracesAction.builderPopup(plugin)
.withContext(DebuggerTraceFileActionContext.class)
.popupWhen(c -> !traceManager.getOpenTraces().isEmpty())
.onAction(c -> traceManager.closeAllTraces())
.buildAndInstallLocal(provider);
.buildAndInstall(tool);
actionCloseOtherTraces = CloseOtherTracesAction.builderPopup(plugin)
.withContext(DebuggerTraceFileActionContext.class)
.popupWhen(c -> traceManager.getOpenTraces().size() > 1 && c.getTrace() != null)
.onAction(c -> traceManager.closeOtherTraces(c.getTrace()))
.buildAndInstallLocal(provider);
.buildAndInstall(tool);
actionCloseDeadTraces = CloseDeadTracesAction.builderPopup(plugin)
.withContext(DebuggerTraceFileActionContext.class)
.popupWhen(c -> !traceManager.getOpenTraces().isEmpty() && targetService != null)
.onAction(c -> traceManager.closeDeadTraces())
.buildAndInstallLocal(provider);
.buildAndInstall(tool);
}
private Trace computeClickedTraceTab(MouseEvent e) {
JList<Trace> list = getList();
int i = list.locationToIndex(e.getPoint());
if (i < 0) {
private String getNameForTrace(Trace trace) {
return DomainObjectDisplayUtils.getTabText(trace);
}
private Icon getIconForTrace(Trace trace) {
if (targetService == null) {
return null;
}
Rectangle cell = list.getCellBounds(i, i);
if (!cell.contains(e.getPoint())) {
Target target = targetService.getTarget(trace);
if (target == null || !target.isValid()) {
return null;
}
return getItem(i);
return DebuggerResources.ICON_RECORD;
}
private Trace setTraceTabActionContext(MouseEvent e) {
Trace newTrace = e == null ? getSelectedItem() : computeClickedTraceTab(e);
actionCloseTrace.getPopupMenuData()
.setMenuItemName(
CloseTraceAction.NAME_PREFIX + (newTrace == null ? "..." : newTrace.getName()));
myActionContext = new DebuggerTraceFileActionContext(newTrace);
provider.traceTabsContextChanged();
return newTrace;
private String getTipForTrace(Trace trace) {
return DomainObjectDisplayUtils.getToolTip(trace);
}
public DebuggerTraceFileActionContext getActionContext() {
return myActionContext;
}
public void coordinatesActivated(DebuggerCoordinates coordinates) {
try (Suppression supp = cbCoordinateActivation.suppress(null)) {
setSelectedItem(coordinates.getTrace());
public DebuggerTraceFileActionContext getActionContext(MouseEvent e) {
if (e == null) {
return null;
}
Trace trace = getValueFor(e);
if (trace == null) {
return null;
}
return new DebuggerTraceFileActionContext(trace);
}
@AutoServiceConsumed
@ -186,28 +157,46 @@ public class DebuggerTraceTabPanel extends HorizontalTabPanel<Trace>
}
}
protected void add(Trace trace) {
addTab(trace);
trace.removeListener(this);
trace.addListener(this);
}
protected void remove(Trace trace) {
trace.removeListener(this);
removeTab(trace);
}
@Override
public void eventSent(PluginEvent event) {
if (Objects.equals(event.getSourceName(), plugin.getName())) {
return;
}
if (event instanceof TraceOpenedPluginEvent evt) {
try (Suppression supp = cbCoordinateActivation.suppress(null)) {
addItem(evt.getTrace());
add(evt.getTrace());
}
}
else if (event instanceof TraceActivatedPluginEvent evt) {
Trace trace = evt.getActiveCoordinates().getTrace();
try (Suppression supp = cbCoordinateActivation.suppress(null)) {
selectTab(trace);
}
}
else if (event instanceof TraceClosedPluginEvent evt) {
Trace trace = evt.getTrace();
try (Suppression supp = cbCoordinateActivation.suppress(null)) {
removeItem(evt.getTrace());
remove(trace);
}
}
}
private void traceTabSelected(ListSelectionEvent e) {
if (e.getValueIsAdjusting()) {
return;
@Override
public void domainObjectChanged(DomainObjectChangedEvent ev) {
if (ev.getSource() instanceof Trace trace) {
refreshTab(trace);
}
Trace newTrace = setTraceTabActionContext(null);
}
private void traceTabSelected(Trace newTrace) {
cbCoordinateActivation.invoke(() -> {
traceManager.activateTrace(newTrace);
});

View file

@ -90,6 +90,12 @@ public class TraceRecorderTarget extends AbstractTarget {
this.recorder = recorder;
}
@Override
public String describe() {
return "%s in %s (recorder)".formatted(getTrace().getDomainFile().getName(),
recorder.getTarget().getModel().getBrief());
}
@Override
public boolean isValid() {
return recorder.isRecording();

View file

@ -410,7 +410,7 @@ public class ProgramModuleIndexer implements DomainFolderChangeListener {
continue;
}
try (PeekOpenedDomainObject peek = new PeekOpenedDomainObject(df)) {
if (programs.contains(peek.object)) {
if (peek.object != null && programs.contains(peek.object)) {
result.add(e);
}
}

View file

@ -26,10 +26,13 @@ import java.util.stream.Stream;
import docking.ActionContext;
import docking.action.DockingAction;
import docking.action.ToggleDockingAction;
import docking.widgets.OptionDialog;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.event.*;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.*;
import ghidra.app.plugin.core.debug.gui.control.DisconnectTask;
import ghidra.app.plugin.core.debug.gui.control.TargetActionTask;
import ghidra.app.services.*;
import ghidra.app.services.DebuggerControlService.ControlModeChangeListener;
import ghidra.async.*;
@ -135,8 +138,8 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
private void threadDeleted(TraceThread thread) {
synchronized (listenersByTrace) {
DebuggerCoordinates last = lastCoordsByTrace.get(trace);
if (last != null && last.getThread() == thread) {
LastCoords last = lastCoordsByTrace.get(trace);
if (last != null && last.coords.getThread() == thread) {
lastCoordsByTrace.remove(trace);
}
}
@ -261,7 +264,20 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
}
}
protected final Map<Trace, DebuggerCoordinates> lastCoordsByTrace = new WeakHashMap<>();
protected record LastCoords(Long time, DebuggerCoordinates coords) {
public static final LastCoords NEVER = new LastCoords(null, DebuggerCoordinates.NOWHERE);
public LastCoords(DebuggerCoordinates coords) {
this(System.currentTimeMillis(), coords);
}
public LastCoords keepTime(DebuggerCoordinates adjusted) {
return new LastCoords(time, adjusted);
}
}
protected final Map<Trace, LastCoords> lastCoordsByTrace = new WeakHashMap<>();
protected final Map<Trace, ListenerForTraceChanges> listenersByTrace = new WeakHashMap<>();
protected final Set<Trace> tracesView = Collections.unmodifiableSet(listenersByTrace.keySet());
@ -275,6 +291,8 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
protected final AsyncReference<Boolean, Void> saveTracesByDefault = new AsyncReference<>(true);
@AutoConfigStateField(codec = BooleanAsyncConfigFieldCodec.class)
protected final AsyncReference<Boolean, Void> autoCloseOnTerminate = new AsyncReference<>(true);
// Do not save this one, it's for testing only
protected boolean ensureActiveTrace = true;
// @AutoServiceConsumed via method
private DebuggerTargetService targetService;
@ -431,37 +449,21 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
@Override
public void closeAllTraces() {
Swing.runIfSwingOrRunLater(() -> {
for (Trace trace : getOpenTraces()) {
closeTrace(trace);
}
});
checkCloseTraces(getOpenTraces());
}
@Override
public void closeOtherTraces(Trace keep) {
Swing.runIfSwingOrRunLater(() -> {
for (Trace trace : getOpenTraces()) {
if (trace != keep) {
closeTrace(trace);
}
}
});
checkCloseTraces(getOpenTraces().stream().filter(t -> t != keep).toList());
}
@Override
public void closeDeadTraces() {
Swing.runIfSwingOrRunLater(() -> {
if (targetService == null) {
return;
}
for (Trace trace : getOpenTraces()) {
Target target = targetService.getTarget(trace);
if (target == null) {
closeTrace(trace);
}
}
});
checkCloseTraces(targetService == null
? getOpenTraces()
: getOpenTraces().stream()
.filter(t -> targetService.getTarget(t) == null)
.toList());
}
@AutoServiceConsumed
@ -549,7 +551,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
}
current = newCurrent;
if (newCurrent.getTrace() != null) {
lastCoordsByTrace.put(newCurrent.getTrace(), newCurrent);
lastCoordsByTrace.put(newCurrent.getTrace(), new LastCoords(newCurrent));
}
}
contextChanged();
@ -610,11 +612,10 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
if (!listenersByTrace.containsKey(trace)) {
return;
}
DebuggerCoordinates cur =
lastCoordsByTrace.getOrDefault(trace, DebuggerCoordinates.NOWHERE);
LastCoords cur = lastCoordsByTrace.getOrDefault(trace, LastCoords.NEVER);
DebuggerCoordinates adj =
cur.platform(getPlatformForMapper(trace, cur.getObject(), mapper));
lastCoordsByTrace.put(trace, adj);
cur.coords.platform(getPlatformForMapper(trace, cur.coords.getObject(), mapper));
lastCoordsByTrace.put(trace, cur.keepTime(adj));
if (trace == current.getTrace()) {
current = adj;
fireLocationEvent(adj, ActivationCause.MAPPER_CHANGED);
@ -670,7 +671,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
synchronized (listenersByTrace) {
// If known, fill in target ASAP, so it determines the time
return fillInTarget(trace,
lastCoordsByTrace.getOrDefault(trace, DebuggerCoordinates.NOWHERE));
lastCoordsByTrace.getOrDefault(trace, LastCoords.NEVER).coords);
}
}
@ -917,6 +918,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
}
new TaskLauncher(new Task("Save New Trace", true, true, true) {
@Override
public void run(TaskMonitor monitor) throws CancelledException {
String filename = trace.getName();
@ -958,6 +960,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
future.completeExceptionally(e);
}
}
});
}
return future;
@ -994,20 +997,64 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
}
}
@Override
public void closeTrace(Trace trace) {
protected void doCloseTraces(Collection<Trace> traces, Collection<Target> targets) {
for (Trace t : traces) {
if (t.getConsumerList().contains(this)) {
firePluginEvent(new TraceClosedPluginEvent(getName(), t));
doTraceClosed(t);
}
}
TargetActionTask.executeTask(tool, new DisconnectTask(tool, targets));
}
protected static final String MSGPAT_TERMINATE = """
<html>
<body width="300px">
<p>This will terminate the following targets:</p>
<ul>
%s
</ul>
<p>Proceed?</p>
</body>
</html>
""";
protected static String formatTargets(Collection<Target> targets) {
return targets.stream()
.map(t -> "<li>%s</li>".formatted(HTMLUtilities.escapeHTML(t.describe())))
.sorted()
.collect(Collectors.joining("\n"));
}
protected void checkCloseTraces(Collection<Trace> traces) {
List<Target> live =
traces.stream()
.map(t -> targetService.getTarget(t))
.filter(t -> t != null)
.toList();
/**
* A provider may be reading the trace, likely via the Swing thread, so schedule this on the
* A provider may be reading a trace, likely via the Swing thread, so schedule this on the
* same thread to avoid a ClosedException.
*/
Swing.runIfSwingOrRunLater(() -> {
if (trace.getConsumerList().contains(this)) {
firePluginEvent(new TraceClosedPluginEvent(getName(), trace));
doTraceClosed(trace);
if (live.isEmpty()) {
doCloseTraces(traces, live);
return;
}
String msg = MSGPAT_TERMINATE.formatted(formatTargets(live));
int response = OptionDialog.showYesNoDialog(null, "Terminate", msg);
switch (response) {
case OptionDialog.YES_OPTION -> doCloseTraces(traces, live);
case OptionDialog.NO_OPTION -> List.of();
}
});
}
@Override
public void closeTrace(Trace trace) {
checkCloseTraces(List.of(trace));
}
@Override
protected void dispose() {
super.dispose();
@ -1034,6 +1081,20 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
return elem.toString();
}
/**
* Gets the most recent coordinates among those traces still open
*/
protected DebuggerCoordinates getMostRecentCoordinates() {
synchronized (listenersByTrace) {
return lastCoordsByTrace.values()
.stream()
.sorted(Comparator.comparing(l -> -l.time))
.findFirst()
.map(l -> l.coords)
.orElse(DebuggerCoordinates.NOWHERE);
}
}
@Override
public CompletableFuture<Void> activateAndNotify(DebuggerCoordinates coordinates,
ActivationCause cause) {
@ -1048,6 +1109,9 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
throw new IllegalStateException(
"Trace must be opened before activated: " + newTrace);
}
if (newTrace == null && ensureActiveTrace) {
coordinates = getMostRecentCoordinates(); // Might still be NOWHERE
}
}
if (cause == ActivationCause.FOLLOW_PRESENT) {
@ -1228,8 +1292,13 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
traces = tracesView.stream().filter(t -> {
ProjectLocator loc = t.getDomainFile().getProjectLocator();
return loc != null && !loc.isTransient();
}).collect(Collectors.toList());
coordsByTrace = Map.copyOf(lastCoordsByTrace);
}).sorted(Comparator.comparingLong(t -> {
LastCoords last = lastCoordsByTrace.get(t);
return last == null ? -1 : last.time;
})).toList();
coordsByTrace = lastCoordsByTrace.entrySet()
.stream()
.collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue().coords));
}
saveState.putInt(KEY_TRACE_COUNT, traces.size());
@ -1246,6 +1315,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
@Override
public void readDataState(SaveState saveState) {
synchronized (listenersByTrace) {
long baseTime = System.currentTimeMillis();
int traceCount = saveState.getInt(KEY_TRACE_COUNT, 0);
for (int index = 0; index < traceCount; index++) {
String stateName = PREFIX_OPEN_TRACE + index;
@ -1253,7 +1323,8 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
DebuggerCoordinates coords =
DebuggerCoordinates.readDataState(tool, saveState, stateName);
if (coords.getTrace() != null) {
lastCoordsByTrace.put(coords.getTrace(), coords);
lastCoordsByTrace.put(coords.getTrace(),
new LastCoords(baseTime + index, coords));
}
}
}

View file

@ -0,0 +1,25 @@
/* ###
* 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.app.plugin.core.debug.gui.listing;
import ghidra.app.plugin.core.debug.gui.trace.DebuggerTraceTabPanel;
public class DebuggerListingProviderTestAccess {
public static DebuggerTraceTabPanel getTraceTabs(DebuggerListingProvider provider) {
return provider.traceTabs;
}
}

View file

@ -40,6 +40,7 @@ import ghidra.app.plugin.core.debug.gui.modules.DebuggerModuleMapProposalDialog.
import ghidra.app.plugin.core.debug.gui.modules.DebuggerModulesProvider.MapModulesAction;
import ghidra.app.plugin.core.debug.gui.modules.DebuggerModulesProvider.MapSectionsAction;
import ghidra.app.plugin.core.debug.gui.modules.DebuggerSectionMapProposalDialog.SectionMapTableColumns;
import ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerServiceTestAccess;
import ghidra.app.services.DebuggerListingService;
import ghidra.debug.api.modules.ModuleMapProposal.ModuleMapEntry;
import ghidra.debug.api.modules.SectionMapProposal.SectionMapEntry;
@ -302,6 +303,7 @@ public class DebuggerModulesProviderLegacyTest extends AbstractGhidraHeadedDebug
@Test
public void testActivatingNoTraceEmptiesProvider() throws Exception {
DebuggerTraceManagerServiceTestAccess.setEnsureActiveTrace(traceManager, false);
createAndOpenTrace();
addModules();

View file

@ -42,6 +42,7 @@ import ghidra.app.plugin.core.debug.gui.modules.DebuggerModuleMapProposalDialog.
import ghidra.app.plugin.core.debug.gui.modules.DebuggerModulesProvider.MapModulesAction;
import ghidra.app.plugin.core.debug.gui.modules.DebuggerModulesProvider.MapSectionsAction;
import ghidra.app.plugin.core.debug.gui.modules.DebuggerSectionMapProposalDialog.SectionMapTableColumns;
import ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerServiceTestAccess;
import ghidra.app.services.DebuggerListingService;
import ghidra.dbg.target.*;
import ghidra.dbg.target.schema.SchemaContext;
@ -434,6 +435,7 @@ public class DebuggerModulesProviderTest extends AbstractGhidraHeadedDebuggerTes
@Test
public void testActivatingNoTraceEmptiesProvider() throws Exception {
DebuggerTraceManagerServiceTestAccess.setEnsureActiveTrace(traceManager, false);
createAndOpenTrace();
addModules();

View file

@ -17,9 +17,7 @@ package ghidra.app.plugin.core.debug.gui.thread;
import static org.junit.Assert.*;
import java.awt.event.MouseEvent;
import java.util.List;
import java.util.Set;
import org.junit.Before;
import org.junit.Test;
@ -29,8 +27,8 @@ import db.Transaction;
import generic.test.category.NightlyCategory;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest;
import ghidra.app.plugin.core.debug.gui.thread.DebuggerLegacyThreadsPanel.ThreadTableColumns;
import ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerServiceTestAccess;
import ghidra.trace.model.Lifespan;
import ghidra.trace.model.Trace;
import ghidra.trace.model.thread.TraceThread;
import ghidra.trace.model.thread.TraceThreadManager;
import ghidra.trace.model.time.TraceTimeManager;
@ -60,32 +58,6 @@ public class DebuggerThreadsProviderLegacyTest extends AbstractGhidraHeadedDebug
}
}
/**
* Check that there exist no tabs, and that the tab row is invisible
*/
protected void assertZeroTabs() {
assertEquals(0, threadsProvider.traceTabs.getList().getModel().getSize());
assertEquals("Tab row should not be visible", 0,
threadsProvider.traceTabs.getVisibleRect().height);
}
/**
* Check that exactly one tab exists, and that the tab row is visible
*/
protected void assertOneTabPopulated() {
assertEquals(1, threadsProvider.traceTabs.getList().getModel().getSize());
assertNotEquals("Tab row should be visible", 0,
threadsProvider.traceTabs.getVisibleRect().height);
}
protected void assertNoTabSelected() {
assertTabSelected(null);
}
protected void assertTabSelected(Trace trace) {
assertEquals(trace, threadsProvider.traceTabs.getSelectedItem());
}
protected void assertThreadsEmpty() {
List<ThreadRow> threadsDisplayed =
threadsProvider.legacyPanel.threadTableModel.getModelData();
@ -122,7 +94,6 @@ public class DebuggerThreadsProviderLegacyTest extends AbstractGhidraHeadedDebug
}
protected void assertProviderEmpty() {
assertZeroTabs();
assertThreadsEmpty();
}
@ -132,45 +103,9 @@ public class DebuggerThreadsProviderLegacyTest extends AbstractGhidraHeadedDebug
assertProviderEmpty();
}
@Test
public void testOpenTracePopupatesTab() throws Exception {
createAndOpenTrace();
waitForSwing();
assertOneTabPopulated();
assertNoTabSelected();
assertThreadsEmpty();
}
@Test
public void testActivateTraceSelectsTab() throws Exception {
createAndOpenTrace();
traceManager.activateTrace(tb.trace);
waitForSwing();
assertOneTabPopulated();
assertTabSelected(tb.trace);
traceManager.activateTrace(null);
waitForSwing();
assertOneTabPopulated();
assertNoTabSelected();
}
@Test
public void testSelectTabActivatesTrace() throws Exception {
createAndOpenTrace();
waitForSwing();
threadsProvider.traceTabs.setSelectedItem(tb.trace);
waitForSwing();
assertEquals(tb.trace, traceManager.getCurrentTrace());
assertEquals(tb.trace, threadsProvider.current.getTrace());
}
@Test
public void testActivateNoTraceEmptiesProvider() throws Exception {
DebuggerTraceManagerServiceTestAccess.setEnsureActiveTrace(traceManager, false);
createAndOpenTrace();
addThreads();
traceManager.activateTrace(tb.trace);
@ -184,22 +119,6 @@ public class DebuggerThreadsProviderLegacyTest extends AbstractGhidraHeadedDebug
assertThreadsEmpty();
}
@Test
public void testCurrentTraceClosedUpdatesTabs() throws Exception {
createAndOpenTrace();
traceManager.activateTrace(tb.trace);
waitForSwing();
assertOneTabPopulated();
assertTabSelected(tb.trace);
traceManager.closeTrace(tb.trace);
waitForSwing();
assertZeroTabs();
assertNoTabSelected();
}
@Test
public void testCurrentTraceClosedEmptiesProvider() throws Exception {
createAndOpenTrace();
@ -215,25 +134,6 @@ public class DebuggerThreadsProviderLegacyTest extends AbstractGhidraHeadedDebug
assertThreadsEmpty();
}
@Test
public void testCloseTraceTabPopupMenuItem() throws Exception {
createAndOpenTrace();
waitForSwing();
assertOneTabPopulated(); // pre-check
clickListItem(threadsProvider.traceTabs.getList(), 0, MouseEvent.BUTTON3);
waitForSwing();
Set<String> expected = Set.of("Close " + tb.trace.getName());
assertMenu(expected, expected);
clickSubMenuItemByText("Close " + tb.trace.getName());
waitForSwing();
waitForPass(() -> {
assertEquals(Set.of(), traceManager.getOpenTraces());
});
}
@Test
public void testActivateThenAddThreadsPopulatesProvider() throws Exception {
createAndOpenTrace();

View file

@ -17,10 +17,8 @@ package ghidra.app.plugin.core.debug.gui.thread;
import static org.junit.Assert.*;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.util.Objects;
import java.util.Set;
import org.junit.*;
import org.junit.experimental.categories.Category;
@ -31,6 +29,7 @@ import generic.test.category.NightlyCategory;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest;
import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.*;
import ghidra.app.plugin.core.debug.gui.model.QueryPanelTestHelper;
import ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerServiceTestAccess;
import ghidra.dbg.target.TargetExecutionStateful;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.dbg.target.schema.SchemaContext;
@ -119,32 +118,6 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerTes
}
}
/**
* Check that there exist no tabs, and that the tab row is invisible
*/
protected void assertZeroTabs() {
assertEquals(0, provider.traceTabs.getList().getModel().getSize());
assertEquals("Tab row should not be visible", 0,
provider.traceTabs.getVisibleRect().height);
}
/**
* Check that exactly one tab exists, and that the tab row is visible
*/
protected void assertOneTabPopulated() {
assertEquals(1, provider.traceTabs.getList().getModel().getSize());
assertNotEquals("Tab row should be visible", 0,
provider.traceTabs.getVisibleRect().height);
}
protected void assertNoTabSelected() {
assertTabSelected(null);
}
protected void assertTabSelected(Trace trace) {
assertEquals(trace, provider.traceTabs.getSelectedItem());
}
protected void assertThreadsTableSize(int size) {
assertEquals(size, provider.panel.getAllItems().size());
}
@ -195,7 +168,6 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerTes
}
protected void assertProviderEmpty() {
assertZeroTabs();
assertThreadsEmpty();
}
@ -218,53 +190,9 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerTes
waitForPass(() -> assertProviderEmpty());
}
@Test
public void testOpenTracePopupatesTab() throws Exception {
createAndOpenTrace();
waitForTasks();
waitForPass(() -> {
assertOneTabPopulated();
assertNoTabSelected();
assertThreadsEmpty();
});
}
@Test
public void testActivateTraceSelectsTab() throws Exception {
createAndOpenTrace();
traceManager.activateTrace(tb.trace);
waitForTasks();
waitForPass(() -> {
assertOneTabPopulated();
assertTabSelected(tb.trace);
});
traceManager.activateTrace(null);
waitForTasks();
waitForPass(() -> {
assertOneTabPopulated();
assertNoTabSelected();
});
}
@Test
public void testSelectTabActivatesTrace() throws Exception {
createAndOpenTrace();
waitForTasks();
provider.traceTabs.setSelectedItem(tb.trace);
waitForTasks();
waitForPass(() -> {
assertEquals(tb.trace, traceManager.getCurrentTrace());
assertEquals(tb.trace, provider.current.getTrace());
});
}
@Test
public void testActivateNoTraceEmptiesProvider() throws Exception {
DebuggerTraceManagerServiceTestAccess.setEnsureActiveTrace(traceManager, false);
createAndOpenTrace();
addThreads();
traceManager.activateTrace(tb.trace);
@ -278,26 +206,6 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerTes
waitForPass(() -> assertThreadsEmpty());
}
@Test
public void testCurrentTraceClosedUpdatesTabs() throws Exception {
createAndOpenTrace();
traceManager.activateTrace(tb.trace);
waitForTasks();
waitForPass(() -> {
assertOneTabPopulated();
assertTabSelected(tb.trace);
});
traceManager.closeTrace(tb.trace);
waitForTasks();
waitForPass(() -> {
assertZeroTabs();
assertNoTabSelected();
});
}
@Test
public void testCurrentTraceClosedEmptiesProvider() throws Exception {
createAndOpenTrace();
@ -313,25 +221,6 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerTes
waitForPass(() -> assertThreadsEmpty());
}
@Test
public void testCloseTraceTabPopupMenuItem() throws Exception {
createAndOpenTrace();
waitForTasks();
waitForPass(() -> assertOneTabPopulated());
clickListItem(provider.traceTabs.getList(), 0, MouseEvent.BUTTON3);
waitForTasks();
Set<String> expected = Set.of("Close " + tb.trace.getName());
assertMenu(expected, expected);
clickSubMenuItemByText("Close " + tb.trace.getName());
waitForTasks();
waitForPass(() -> {
assertEquals(Set.of(), traceManager.getOpenTraces());
});
}
@Test
public void testActivateThenAddThreadsPopulatesProvider() throws Exception {
createAndOpenTrace();

View file

@ -0,0 +1,111 @@
/* ###
* 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.app.plugin.core.debug.gui.trace;
import static org.junit.Assert.assertEquals;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest;
import ghidra.app.plugin.core.debug.gui.listing.*;
import ghidra.trace.database.ToyDBTraceBuilder;
public class DebuggerTraceTabPanelTest extends AbstractGhidraHeadedDebuggerTest {
private DebuggerTraceTabPanel traceTabs;
@Before
public void setUpTabTest() throws Throwable {
addPlugin(tool, DebuggerListingPlugin.class);
DebuggerListingProvider listingProvider =
waitForComponentProvider(DebuggerListingProvider.class);
traceTabs = DebuggerListingProviderTestAccess.getTraceTabs(listingProvider);
}
@Test
public void testEmpty() {
assertEquals(List.of(), traceTabs.getTabValues());
}
@Test
public void testOpenTraceAddsTab() throws Throwable {
createAndOpenTrace();
waitForSwing();
assertEquals(List.of(tb.trace), traceTabs.getTabValues());
}
@Test
public void testActivateTraceSelectsTab() throws Throwable {
try (
ToyDBTraceBuilder tb1 = new ToyDBTraceBuilder(getName() + "_1", LANGID_TOYBE64);
ToyDBTraceBuilder tb2 = new ToyDBTraceBuilder(getName() + "_2", LANGID_TOYBE64)) {
traceManager.openTrace(tb1.trace);
traceManager.openTrace(tb2.trace);
waitForSwing();
traceManager.activateTrace(tb1.trace);
waitForSwing();
assertEquals(tb1.trace, traceTabs.getSelectedTabValue());
traceManager.activateTrace(tb2.trace);
waitForSwing();
assertEquals(tb2.trace, traceTabs.getSelectedTabValue());
}
}
@Test
public void testSelectTabActivatesTrace() throws Throwable {
try (
ToyDBTraceBuilder tb1 = new ToyDBTraceBuilder(getName() + "_1", LANGID_TOYBE64);
ToyDBTraceBuilder tb2 = new ToyDBTraceBuilder(getName() + "_2", LANGID_TOYBE64)) {
traceManager.openTrace(tb1.trace);
traceManager.openTrace(tb2.trace);
waitForSwing();
traceTabs.selectTab(tb1.trace);
waitForSwing();
assertEquals(tb1.trace, traceManager.getCurrentTrace());
traceTabs.selectTab(tb2.trace);
waitForSwing();
assertEquals(tb2.trace, traceManager.getCurrentTrace());
}
}
@Test
public void testCloseTraceRemovesTab() throws Throwable {
try (
ToyDBTraceBuilder tb1 = new ToyDBTraceBuilder(getName() + "_1", LANGID_TOYBE64);
ToyDBTraceBuilder tb2 = new ToyDBTraceBuilder(getName() + "_2", LANGID_TOYBE64)) {
traceManager.openTrace(tb1.trace);
traceManager.openTrace(tb2.trace);
waitForSwing();
assertEquals(List.of(tb1.trace, tb2.trace), traceTabs.getTabValues());
traceManager.closeTrace(tb1.trace);
waitForSwing();
assertEquals(List.of(tb2.trace), traceTabs.getTabValues());
traceManager.closeTrace(tb2.trace);
waitForSwing();
assertEquals(List.of(), traceTabs.getTabValues());
}
}
}

View file

@ -46,6 +46,11 @@ class MockTarget implements Target {
this.trace = trace;
}
@Override
public String describe() {
return "Mock Target";
}
@Override
public boolean isValid() {
return true;

View file

@ -141,6 +141,7 @@ public class DebuggerTraceManagerServiceTest extends AbstractGhidraHeadedDebugge
assertEquals(thread, traceManager.getCurrentThread());
DebuggerTraceManagerServiceTestAccess.setEnsureActiveTrace(traceManager, false);
traceManager.activateTrace(null);
waitForSwing();
@ -177,6 +178,7 @@ public class DebuggerTraceManagerServiceTest extends AbstractGhidraHeadedDebugge
assertEquals(5, traceManager.getCurrentSnap());
DebuggerTraceManagerServiceTestAccess.setEnsureActiveTrace(traceManager, false);
traceManager.activateTrace(null);
waitForSwing();
@ -207,6 +209,7 @@ public class DebuggerTraceManagerServiceTest extends AbstractGhidraHeadedDebugge
assertEquals(5, traceManager.getCurrentFrame());
DebuggerTraceManagerServiceTestAccess.setEnsureActiveTrace(traceManager, false);
traceManager.activateTrace(null);
waitForSwing();
@ -251,6 +254,7 @@ public class DebuggerTraceManagerServiceTest extends AbstractGhidraHeadedDebugge
assertEquals(objThread0, traceManager.getCurrentObject());
assertEquals(thread, traceManager.getCurrentThread());
DebuggerTraceManagerServiceTestAccess.setEnsureActiveTrace(traceManager, false);
traceManager.activateTrace(null);
waitForSwing();

View file

@ -0,0 +1,24 @@
/* ###
* 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.app.plugin.core.debug.service.tracemgr;
import ghidra.app.services.DebuggerTraceManagerService;
public class DebuggerTraceManagerServiceTestAccess {
public static void setEnsureActiveTrace(DebuggerTraceManagerService traceManager, boolean b) {
((DebuggerTraceManagerServicePlugin) traceManager).ensureActiveTrace = b;
}
}

View file

@ -1,202 +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 docking.widgets;
import java.awt.*;
import java.awt.event.ActionEvent;
import javax.swing.*;
import javax.swing.border.BevelBorder;
import javax.swing.event.ChangeEvent;
// TODO: I'd like "close" buttons on the tabs, optionally.
// For now, client must use a popup menu.
@SuppressWarnings("serial")
public class HorizontalTabPanel<T> extends JPanel {
public static Color copyColor(Color c) {
return new Color(c.getRGB());
}
public static class TabListCellRenderer<T> implements ListCellRenderer<T> {
protected final Box hBox = Box.createHorizontalBox();
protected final JLabel label = new JLabel();
{
hBox.setBorder(new BevelBorder(BevelBorder.RAISED));
hBox.setOpaque(true);
hBox.add(label);
}
protected String getText(T value) {
return value.toString();
}
protected Icon getIcon(T value) {
return null;
}
@Override
public Component getListCellRendererComponent(JList<? extends T> list,
T value, int index, boolean isSelected, boolean cellHasFocus) {
label.setText(getText(value));
label.setIcon(getIcon(value));
if (isSelected) {
//label.setForeground(list.getSelectionForeground());
label.setForeground(copyColor(list.getSelectionForeground()));
hBox.setBackground(list.getSelectionBackground());
}
else {
label.setForeground(list.getForeground());
hBox.setBackground(list.getBackground());
}
hBox.validate();
return hBox;
}
}
protected final JList<T> list = new JList<>();
private final JScrollPane scroll = new JScrollPane(list);
private final JViewport viewport = scroll.getViewport();
private final DefaultListModel<T> model = new DefaultListModel<>();
private final JButton left = new JButton("<");
private final JButton right = new JButton(">");
{
list.setModel(model);
// TODO: Experiment with multiple traces in one timeline
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
list.setLayoutOrientation(JList.HORIZONTAL_WRAP);
list.setVisibleRowCount(1);
list.setCellRenderer(new TabListCellRenderer<>());
list.setOpaque(false);
scroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER);
scroll.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
scroll.setBorder(null);
viewport.addChangeListener(this::viewportChanged);
left.setBorder(null);
right.setBorder(null);
left.setContentAreaFilled(false);
right.setContentAreaFilled(false);
left.setOpaque(true);
right.setOpaque(true);
left.addActionListener(this::leftActivated);
right.addActionListener(this::rightActivated);
}
public HorizontalTabPanel() {
super();
setLayout(new BorderLayout());
list.setBackground(getBackground());
add(scroll, BorderLayout.CENTER);
add(left, BorderLayout.WEST);
add(right, BorderLayout.EAST);
}
private void viewportChanged(ChangeEvent e) {
Dimension paneSize = getSize();
Dimension listSize = list.getSize();
boolean buttonsVisible = paneSize.getWidth() < listSize.getWidth();
left.setVisible(buttonsVisible);
right.setVisible(buttonsVisible);
}
/**
* Find the first cell which is even partially visible
*
* @param reverse true to search from right to left
* @return the cell index
*/
private int findFirstVisible(boolean reverse) {
int n = model.getSize();
Rectangle vis = list.getVisibleRect();
for (int i = reverse ? n - 1 : 0; reverse ? i >= 0 : i < n; i += reverse ? -1 : 1) {
Rectangle b = list.getCellBounds(i, i);
if (vis.intersects(b)) {
return i;
}
}
return -1;
}
/**
* Find the first cell <em>after</em> a given start which is even partially occluded
*
* @param start the starting cell index
* @param reverse true to search from right to left
* @return the cell index
*/
private int findNextOccluded(int start, boolean reverse) {
if (start == -1) {
return -1;
}
int n = model.getSize();
Rectangle vis = list.getVisibleRect();
for (int i = reverse ? start - 1 : start + 1; reverse ? i >= 0 : i < n; i +=
reverse ? -1 : 1) {
Rectangle b = list.getCellBounds(i, i);
if (!vis.contains(b)) {
return i;
}
}
return -1;
}
private void leftActivated(ActionEvent e) {
list.ensureIndexIsVisible(findNextOccluded(findFirstVisible(true), true));
}
private void rightActivated(ActionEvent e) {
list.ensureIndexIsVisible(findNextOccluded(findFirstVisible(false), false));
}
public JList<T> getList() {
return list;
}
public void addItem(T item) {
model.addElement(item);
revalidate();
}
public void removeItem(T item) {
model.removeElement(item);
revalidate();
}
public T getSelectedItem() {
int index = list.getSelectedIndex();
return index < 0 ? null : list.getModel().getElementAt(index);
}
public void setSelectedItem(T item) {
// NOTE: For large lists, this could get slow
int index = model.indexOf(item);
if (index < 0) {
list.clearSelection();
}
else {
list.setSelectedIndex(index);
}
}
public T getItem(int index) {
return list.getModel().getElementAt(index);
}
}

View file

@ -222,51 +222,12 @@ public class MultiTabPlugin extends Plugin implements DomainObjectListener, Opti
}
}
String getStringUsedInList(Program program) {
DomainFile df = program.getDomainFile();
String changeIndicator = program.isChanged() ? "*" : "";
String pathString = getShortPath(df);
if (!df.isInWritableProject()) {
return pathString + " [Read-Only]" + changeIndicator;
}
return pathString + changeIndicator;
private String getToolTip(Program program) {
return DomainObjectDisplayUtils.getToolTip(program);
}
private String getShortPath(DomainFile df) {
String pathString = df.toString();
int length = pathString.length();
if (length < 100) {
return pathString;
}
String[] pathParts = pathString.split("/");
if (pathParts.length == 2) { // at least 2 for project name and filename
return pathString;
}
String projectName = df.getProjectLocator().getName();
int parentFolderIndex = pathParts.length - 2;
String parentName = pathParts[parentFolderIndex];
String filename = df.getName();
pathString = projectName + ":/.../" + parentName + "/" + filename;
return pathString;
}
String getToolTip(Program program) {
return getStringUsedInList(program);
}
String getName(Program program) {
DomainFile df = program.getDomainFile();
String tabName = df.getName();
if (df.isReadOnly()) {
int version = df.getVersion();
if (!df.canSave() && version != DomainFile.DEFAULT_VERSION) {
tabName += "@" + version;
}
tabName = tabName + " [Read-Only]";
}
return tabName;
private String getTabName(Program program) {
return DomainObjectDisplayUtils.getTabText(program);
}
void keyTypedFromListWindow(KeyEvent e) {
@ -334,22 +295,6 @@ public class MultiTabPlugin extends Plugin implements DomainObjectListener, Opti
return EMPTY8_ICON;
}
private String getTabName(Program program) {
DomainFile df = program.getDomainFile();
String tabName = df.getName();
if (df.isReadOnly()) {
int version = df.getVersion();
if (!df.canSave() && version != DomainFile.DEFAULT_VERSION) {
tabName += "@" + version;
}
tabName = tabName + " [Read-Only]";
}
if (program.isChanged()) {
tabName = "*" + tabName;
}
return tabName;
}
boolean removeProgram(Program program) {
return progService.closeProgram(program, false);
}

View file

@ -0,0 +1,79 @@
/* ###
* 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.framework.model;
import ghidra.framework.store.FileSystem;
public class DomainObjectDisplayUtils {
private static final String VERSION_SEP = "@";
private static final String CHANGE_INDICATOR = "*";
private static final String READ_ONLY = " [Read-Only]";
private static final String PROJECT_SEP_ELLIPSES =
":" + FileSystem.SEPARATOR + "..." + FileSystem.SEPARATOR;
private DomainObjectDisplayUtils() {
}
public static String getShortPath(DomainFile df) {
String pathString = df.toString();
int length = pathString.length();
if (length < 100) {
return pathString;
}
String[] pathParts = pathString.split(FileSystem.SEPARATOR);
if (pathParts.length == 2) { // at least 2 for project name and filename
return pathString;
}
String projectName = df.getProjectLocator().getName();
int parentFolderIndex = pathParts.length - 2;
String parentName = pathParts[parentFolderIndex];
String filename = df.getName();
pathString =
projectName + PROJECT_SEP_ELLIPSES + parentName + FileSystem.SEPARATOR + filename;
return pathString;
}
public static String getToolTip(DomainObject object) {
DomainFile df = object.getDomainFile();
String changeIndicator = object.isChanged() ? CHANGE_INDICATOR : "";
String pathString = getShortPath(df);
if (!df.isInWritableProject()) {
return pathString + READ_ONLY + changeIndicator;
}
return pathString + changeIndicator;
}
public static String getTabText(DomainFile df) {
String tabName = df.getName();
if (df.isReadOnly()) {
int version = df.getVersion();
if (!df.canSave() && version != DomainFile.DEFAULT_VERSION) {
tabName += VERSION_SEP + version;
}
tabName = tabName + READ_ONLY;
}
return tabName;
}
public static String getTabText(DomainObject object) {
if (object.isChanged()) {
return CHANGE_INDICATOR + getTabText(object.getDomainFile());
}
return getTabText(object.getDomainFile());
}
}

View file

@ -24,6 +24,7 @@ import org.junit.Test;
import db.Transaction;
import generic.Unique;
import ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerServiceTestAccess;
import ghidra.app.script.GhidraState;
import ghidra.app.services.DebuggerTraceManagerService;
import ghidra.debug.api.breakpoint.LogicalBreakpoint;
@ -172,6 +173,7 @@ public class DeadFlatDebuggerAPITest extends AbstractFlatDebuggerAPITest<FlatDeb
@Test
public void testActivateTraceNull() throws Throwable {
DebuggerTraceManagerServiceTestAccess.setEnsureActiveTrace(traceManager, false);
createAndOpenTrace();
traceManager.activateTrace(tb.trace);
waitForSwing();
@ -217,6 +219,7 @@ public class DeadFlatDebuggerAPITest extends AbstractFlatDebuggerAPITest<FlatDeb
@Test
public void testActivateThreadNull() throws Throwable {
DebuggerTraceManagerServiceTestAccess.setEnsureActiveTrace(traceManager, false);
api.activateThread(null);
assertEquals(null, traceManager.getCurrentThread());