From f7e243e46089d5c692a2caa9e4613ae55b89caef Mon Sep 17 00:00:00 2001 From: ghidra_red Date: Thu, 12 Mar 2026 15:55:49 +0000 Subject: [PATCH] GP-5893: Added GTreeTable, BreakpointTimelineActions for quickly navigating snaps in a trace, TraceCallTreePlugin for viewing a call tree of the given trace, TenetPlusPlusLoader for ingesting Tenet++ trace files --- .../app/services/DebuggerListingService.java | 24 + .../ghidra/app/services/ProgressService.java | 10 +- .../debug/api/listing/DebuggerListing.java | 61 + .../DebuggerMissingModuleActionContext.java | 17 +- .../app/util/opinion/TenetPlusPlusLoader.java | 600 ++++++++ .../util/opinion/TenetLoaderPlusPlusTest.java | 393 +++++ .../timeline/BreakpointTimelineActions.java | 376 +++++ .../timeline/BreakpointTimelinePanel.java | 51 +- .../timeline/BreakpointTimelinePlugin.java | 7 +- .../timeline/BreakpointTimelineProvider.java | 64 +- .../gui/listing/DebuggerListingPlugin.java | 29 + .../gui/listing/DebuggerListingProvider.java | 10 +- .../gui/modules/DebuggerModulesProvider.java | 9 +- .../BreakpointTimeOverviewColorService.java | 33 +- .../AbstractTraceCallTreeNode.java | 101 ++ .../TraceCallTreeActionContext.java | 35 + .../tracecalltree/TraceCallTreeCallNode.java | 27 + .../TraceCallTreeExternalNode.java | 34 + .../TraceCallTreeLogContext.java | 57 + .../tracecalltree/TraceCallTreeLogModel.java | 163 +++ .../gui/tracecalltree/TraceCallTreeModel.java | 142 ++ .../tracecalltree/TraceCallTreePlugin.java | 90 ++ .../tracecalltree/TraceCallTreeProvider.java | 1297 +++++++++++++++++ .../TraceCallTreeReturnNode.java | 34 + .../gui/tracecalltree/TraceCallTreeTable.java | 44 + .../TraceCallTreeTailCallNode.java | 34 + .../DebuggerStaticMappingServicePlugin.java | 123 +- .../DynamicStaticSynchronizationPlugin.java | 2 +- .../Docking/data/docking.theme.properties | 3 + .../java/docking/util/MessageOverlay.java | 75 + .../widgets/gtreetable/GTreeTable.java | 104 ++ .../gtreetable/GTreeTableCellRenderer.java | 115 ++ .../widgets/gtreetable/GTreeTableModel.java | 153 ++ .../widgets/gtreetable/GTreeTableNode.java | 491 +++++++ 34 files changed, 4739 insertions(+), 69 deletions(-) create mode 100644 Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/listing/DebuggerListing.java create mode 100644 Ghidra/Debug/Debugger-importers/src/main/java/ghidra/app/util/opinion/TenetPlusPlusLoader.java create mode 100644 Ghidra/Debug/Debugger-importers/src/test/java/ghidra/app/util/opinion/TenetLoaderPlusPlusTest.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/timeline/BreakpointTimelineActions.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/tracecalltree/AbstractTraceCallTreeNode.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/tracecalltree/TraceCallTreeActionContext.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/tracecalltree/TraceCallTreeCallNode.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/tracecalltree/TraceCallTreeExternalNode.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/tracecalltree/TraceCallTreeLogContext.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/tracecalltree/TraceCallTreeLogModel.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/tracecalltree/TraceCallTreeModel.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/tracecalltree/TraceCallTreePlugin.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/tracecalltree/TraceCallTreeProvider.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/tracecalltree/TraceCallTreeReturnNode.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/tracecalltree/TraceCallTreeTable.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/tracecalltree/TraceCallTreeTailCallNode.java create mode 100644 Ghidra/Framework/Docking/src/main/java/docking/util/MessageOverlay.java create mode 100644 Ghidra/Framework/Docking/src/main/java/docking/widgets/gtreetable/GTreeTable.java create mode 100644 Ghidra/Framework/Docking/src/main/java/docking/widgets/gtreetable/GTreeTableCellRenderer.java create mode 100644 Ghidra/Framework/Docking/src/main/java/docking/widgets/gtreetable/GTreeTableModel.java create mode 100644 Ghidra/Framework/Docking/src/main/java/docking/widgets/gtreetable/GTreeTableNode.java diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/DebuggerListingService.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/DebuggerListingService.java index d4e90cbecb..bdb764ed3a 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/DebuggerListingService.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/DebuggerListingService.java @@ -15,9 +15,12 @@ */ package ghidra.app.services; +import java.util.List; + import ghidra.app.util.viewer.listingpanel.ListingPanel; import ghidra.debug.api.action.AutoReadMemorySpec; import ghidra.debug.api.action.LocationTrackingSpec; +import ghidra.debug.api.listing.DebuggerListing; import ghidra.debug.api.listing.MultiBlendedListingBackgroundColorModel; import ghidra.framework.plugintool.ServiceInfo; import ghidra.program.model.address.Address; @@ -57,6 +60,20 @@ public interface DebuggerListingService extends CodeViewerService { */ LocationTrackingSpec getTrackingSpec(); + /** + * Create a new disconnected debugger listing + * + * @return newly created debugger listing + */ + DebuggerListing createNewListing(); + + /** + * Get all debugger listings + * + * @return List of all debugger listings + */ + List getAllListings(); + /** * Get the auto-read memory specification of the main listing. * @@ -108,4 +125,11 @@ public interface DebuggerListingService extends CodeViewerService { */ MultiBlendedListingBackgroundColorModel createListingBackgroundColorModel( ListingPanel listingPanel); + + /** + * Get the next custom title for a DebuggerListing + * + * @return Next custom title + */ + String findNextCustomTitle(); } diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/ProgressService.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/ProgressService.java index 2a57f15a5e..9c96f8ab8a 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/ProgressService.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/ProgressService.java @@ -4,9 +4,9 @@ * 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. @@ -52,7 +52,8 @@ import ghidra.util.task.TaskMonitor; * can determine how to present them. */ @ServiceInfo( - defaultProviderName = "ghidra.app.plugin.core.debug.service.progress.ProgressServicePlugin") + defaultProviderName = "ghidra.app.plugin.core.debug.service.progress.ProgressServicePlugin" +) public interface ProgressService { /** * Publish a task and create a monitor for it @@ -150,6 +151,9 @@ public interface ProgressService { monitor.setCancelEnabled(canCancel); CompletableFuture future = futureSupplier.apply(monitor); future.handle((t, ex) -> { + if (canCancel) { + monitor.cancel(); + } monitor.close(); return null; }); diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/listing/DebuggerListing.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/listing/DebuggerListing.java new file mode 100644 index 0000000000..7611d1472a --- /dev/null +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/listing/DebuggerListing.java @@ -0,0 +1,61 @@ +/* ### + * 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.debug.api.listing; + +import ghidra.app.nav.Navigatable; +import ghidra.debug.api.action.LocationTrackingSpec; + +public interface DebuggerListing extends Navigatable { + /** + * Get the window title of this debugger listing + * + * @return Title of window + */ + String getTitle(); + + /** + * Returns boolean if this listing is the main debugger listing + * + * @return true/false if this is the main listing + */ + boolean isMainListing(); + + /** + * Set a custom title. + *

+ * Setting the title here prevents future calls to + * {@link docking.ComponentProvider#setTitle(String)} from having any effect. This is done to + * preserve the custom + * title. + * + * @param title the title + */ + void setCustomTitle(String title); + + /** + * Set if this debugger listing should follow the current thread when displaying + * + * @param follows true/false if this listing should follow the current thread + */ + void setFollowsCurrentThread(boolean follows); + + /** + * Set what this debugger listing should track as the user performs actions + * + * @param spec {@link LocationTrackingSpec} describing how/what the listing will track + */ + void setTrackingSpec(LocationTrackingSpec spec); +} diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/modules/DebuggerMissingModuleActionContext.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/modules/DebuggerMissingModuleActionContext.java index 345fbc6ebc..4e4bd43af9 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/modules/DebuggerMissingModuleActionContext.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/modules/DebuggerMissingModuleActionContext.java @@ -4,9 +4,9 @@ * 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. @@ -22,17 +22,23 @@ import ghidra.trace.model.modules.TraceModule; public class DebuggerMissingModuleActionContext extends DefaultActionContext { private final TraceModule module; + private final long snap; private final int hashCode; - public DebuggerMissingModuleActionContext(TraceModule module) { + public DebuggerMissingModuleActionContext(TraceModule module, long snap) { this.module = Objects.requireNonNull(module); - this.hashCode = Objects.hash(getClass(), module); + this.hashCode = Objects.hash(getClass(), module, snap); + this.snap = snap; } public TraceModule getModule() { return module; } + public long getSnap() { + return snap; + } + @Override public int hashCode() { return hashCode; @@ -49,6 +55,9 @@ public class DebuggerMissingModuleActionContext extends DefaultActionContext { if (!this.module.equals(that.module)) { return false; } + if (this.snap != that.snap) { + return false; + } return true; } } diff --git a/Ghidra/Debug/Debugger-importers/src/main/java/ghidra/app/util/opinion/TenetPlusPlusLoader.java b/Ghidra/Debug/Debugger-importers/src/main/java/ghidra/app/util/opinion/TenetPlusPlusLoader.java new file mode 100644 index 0000000000..bba45ff796 --- /dev/null +++ b/Ghidra/Debug/Debugger-importers/src/main/java/ghidra/app/util/opinion/TenetPlusPlusLoader.java @@ -0,0 +1,600 @@ +/* ### + * 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.util.opinion; + +import java.io.*; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.jdom2.JDOMException; + +import db.Transaction; +import ghidra.app.util.Option; +import ghidra.app.util.bin.ByteProvider; +import ghidra.app.util.importer.MessageLog; +import ghidra.framework.model.DomainFile; +import ghidra.framework.model.DomainObject; +import ghidra.program.database.ProgramContentHandler; +import ghidra.program.model.address.*; +import ghidra.program.model.lang.*; +import ghidra.program.model.listing.Program; +import ghidra.program.model.symbol.RefType; +import ghidra.program.model.symbol.SourceType; +import ghidra.program.util.AddressEvaluator; +import ghidra.trace.database.DBTrace; +import ghidra.trace.database.target.DBTraceObjectManager; +import ghidra.trace.model.Lifespan; +import ghidra.trace.model.Trace; +import ghidra.trace.model.memory.TraceMemoryFlag; +import ghidra.trace.model.modules.TraceModule; +import ghidra.trace.model.modules.TraceModuleManager; +import ghidra.trace.model.stack.TraceStackFrame; +import ghidra.trace.model.target.TraceObject; +import ghidra.trace.model.target.TraceObject.ConflictResolution; +import ghidra.trace.model.target.TraceObjectManager; +import ghidra.trace.model.target.TraceObjectManager.BypassWriteCache; +import ghidra.trace.model.target.path.KeyPath; +import ghidra.trace.model.target.schema.*; +import ghidra.trace.model.target.schema.TraceObjectSchema.SchemaName; +import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.time.TraceSnapshot; +import ghidra.util.Msg; +import ghidra.util.NumericUtilities; +import ghidra.util.exception.*; +import ghidra.util.task.TaskMonitor; + +public class TenetPlusPlusLoader implements Loader { + private record ModuleEvent(String path, String module, AddressRange range) {} + + private static final String TRACE_SUFFIX = ".trace"; + private static final String TENET_SUFFIX = ".tenet"; + private static final Set SUFFIXES = Set.of(TRACE_SUFFIX, TENET_SUFFIX); + + private static final Pattern TID_PATTERN = Pattern.compile("tid=([0-9a-fA-F]+),"); + private static final Pattern REG_PATTERN = + Pattern.compile("(?:^|,)(?!(?:mr|mw|ma|slide)=)([a-zA-Z0-9]+)=0x([0-9a-fA-F]+)"); + private static final Pattern MEM_PATTERN = + Pattern.compile("(?:^|,)(mr|mw|ma)=0x([^:]+):([0-9a-fA-F]+)"); + private static final Pattern MODULE_LOAD_PATTERN = + Pattern.compile("Loaded image: 0x([0-9a-fA-F]+):0x([0-9a-fA-F]+) -> (.*)$"); + private static final Pattern MODULE_UNLOAD_PATTERN = + Pattern.compile("Unloaded image: 0x([0-9a-fA-F]+):0x([0-9a-fA-F]+) -> (.*)$"); + + static final int ERROR_THRESHOLD = 10; + + static final String DOMAIN_FILE_OPTION_NAME = "Program to associate trace file with"; + + private static final String TENET_PLUS_PLUS_CTX_XML = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """; + private static final SchemaContext SCHEMA_CTX; + private static final TraceObjectSchema TENET_PLUS_PLUS_SESSION_SCHEMA; + + static { + try { + SCHEMA_CTX = XmlSchemaContext.deserialize(TENET_PLUS_PLUS_CTX_XML); + } + catch (final JDOMException e) { + throw new AssertionError(e); + } + TENET_PLUS_PLUS_SESSION_SCHEMA = + SCHEMA_CTX.getSchema(new SchemaName("TenetPlusPlusSession")); + } + + private String STORE_REGS_AS_ATTRS_OPTION_NAME = "Store registers as attributes in model"; + private boolean storeRegsAsAttrs = false; + private String ADD_ALL_MEM_REFS_OPTION_NAME = "Add all memory references"; + private boolean addAllMemRefs = false; + + private static boolean isTenetPlusPlusFile(final ByteProvider provider) { + final String nameLower = provider.getName().toLowerCase(); + + if (!SUFFIXES.stream().anyMatch(nameLower::endsWith)) { + return false; + } + + try { + final byte[] bytes = new byte[1000]; + provider.getInputStream(0).read(bytes); + final String[] lines = new String(bytes, StandardCharsets.UTF_8).split("\n"); + + // Skip last line as it could be a partial one + for (int i = 0; i < (lines.length - 1); i++) { + final String line = lines[i]; + if ((!MODULE_LOAD_PATTERN.matcher(line).find() && + !MODULE_UNLOAD_PATTERN.matcher(line).find()) && + (!TID_PATTERN.matcher(line).find() && !REG_PATTERN.matcher(line).find())) { + return false; + } + } + + return true; + } + catch (final IOException e) { + return false; + } + } + + private AddressSpace defaultSpace; + + private Program program; + + private void addModuleEventsToSnap(Trace trace, Long snap, List load, + List unload) throws DuplicateNameException { + final TraceModuleManager modMan = trace.getModuleManager(); + if (!unload.isEmpty()) { + for (final ModuleEvent event : unload) { + final TraceModule module = modMan.getLoadedModuleByPath(snap, event.path()); + if (module != null) { + module.remove(snap); + } + } + unload.clear(); + } + + if (!load.isEmpty()) { + for (final ModuleEvent event : load) { + modMan.addLoadedModule(event.path, event.module, event.range, snap); + } + + load.clear(); + } + } + + /** + * Create an address in the processor's default space. + * + * @param offset the byte offset + * @return the address + */ + private Address addr(final long offset) { + return defaultSpace.getAddress(offset); + } + + @Override + public Collection findSupportedLoadSpecs(final ByteProvider provider) + throws IOException { + final List loadSpecs = new ArrayList<>(); + if (isTenetPlusPlusFile(provider)) { + // TODO: This LanguageCompilerSpecPair is not used by the loader + // just putting a dummy value in so the user doesn't have to + loadSpecs.add(new LoadSpec(this, 0, + new LanguageCompilerSpecPair("DATA:LE:64:default", "pointer64"), true)); + } + return loadSpecs; + } + + @Override + public List

+ * This is basically syntactic sugar, since expressing a byte array literal can get obtuse in + * Java. {@code new byte[] {0, 1, 2, (byte) 0x80, (byte) 0xff}} vs + * {@code arr(0, 1, 2, 0x80, 0xff)}. + * + * @param e the bytes' values + * @return the array + */ + private byte[] arr(final int... e) { + final byte[] result = new byte[e.length]; + for (int i = 0; i < e.length; i++) { + result[i] = (byte) e[i]; + } + return result; + } + + private Trace createTraceWithLoader(final String testFile) throws Exception { + this.createProgram(); + intoProject(program); + + final TenetPlusPlusLoader loader = new TenetPlusPlusLoader(); + final ByteProvider provider = + new ByteArrayProvider("test.tenet", testFile.getBytes("utf8")); + final LoadSpec loadSpec = Unique.assertOne(loader.findSupportedLoadSpecs(provider)); + + final List

    + *
  1. Display in main debugger listing actions
  2. + *
  3. Display in one of the other additional existing debugger listings
  4. + *
  5. Display in a new listing
  6. + *
+ * + * @param tool Tool to add actions to + * @param result List of resulting actions to add to + * @param name Name of action + * @param subgroup Subgroup of action + * @param address Target address to use for action + */ + private void createActionsToViewVariableInListings(Tool tool, + final List result, String name, String subgroup, + final Address address) { + // Add action for main debugger listing window + result.add(TraceCallTreePopupAction + .builder(this, TraceCallTreePopupAction.GROUP3, subgroup, "View " + name) + .onAction(ctx -> { + if (listingService == null) { + return; + } + final ProgramLocation loc = + new ProgramLocation(currentCoords.getView(), address); + listingService.goTo(loc, true); + }) + .build()); + + // Add action for each additional debugger listing window + tool.setMenuGroup(new String[] { name }, TraceCallTreePopupAction.GROUP3, + "extra" + subgroup); + for (final DebuggerListing dl : listingService.getAllListings()) { + if (dl.isMainListing()) { + continue; + } + + result.add(TraceCallTreePopupAction + .builder(this, TraceCallTreePopupAction.GROUP3, subgroup, name, + "View In " + dl.getTitle()) + .onAction(ctx -> { + if (listingService == null) { + return; + } + final ProgramLocation loc = + new ProgramLocation(currentCoords.getView(), address); + dl.goTo(loc); + }) + .build()); + } + + // Add action to view in new listing + result.add(TraceCallTreePopupAction + .builder(this, TraceCallTreePopupAction.GROUP4, subgroup, name, + "View In New Listing") + .onAction(ctx -> { + if (listingService == null) { + return; + } + final DebuggerListing dl = listingService.createNewListing(); + dl.setCustomTitle(listingService.findNextCustomTitle()); + dl.setTrackingSpec(NoneLocationTrackingSpec.INSTANCE); + dl.setFollowsCurrentThread(true); + final ProgramLocation loc = + new ProgramLocation(currentCoords.getView(), address); + dl.goTo(loc); + }) + .build()); + } + + private CompletableFuture createCallTree(TaskMonitor monitor, + DebuggerCoordinates coords) { + return CompletableFuture.supplyAsync(() -> { + try { + return doCreateCallTree(monitor, coords); + } + catch (final CancelledException | TraceClosedException e) { + return null; + } + }); + } + + protected JSplitPane createTbSplit() { + final JSplitPane split = new JSplitPane(JSplitPane.VERTICAL_SPLIT); + split.setResizeWeight(tbResizeWeight); + split.addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, pce -> { + tbResizeWeight = computeResizeWeight(split); + }); + return split; + } + + public void dispose() { + if (mappingService != null) { + mappingService.removeChangeListener(listener); + } + if ((callTreeNodeCompletableFuture != null) && !callTreeNodeCompletableFuture.isDone()) { + callTreeNodeCompletableFuture.cancel(true); + } + + treeTableModel.removeThreadedTableModelListener(modelListenerAdapter); + } + + /** + * Create the call tree + * + * @param monitor TaskMonitor for reporting status + * @param coords Current debugger coordinates + * @return null or built AbstractTraceCallTreeNode + * @throws CancelledException Task is canceled + */ + private AbstractTraceCallTreeNode doCreateCallTree(TaskMonitor monitor, + DebuggerCoordinates coords) throws CancelledException { + if (mappingService == null) { + return null; + } + + final Trace trace = coords.getTrace(); + final TraceThread thread = coords.getThread(); + + if ((trace == null) || (thread == null)) { + return null; + } + + consoleService.removeFromLog(new TraceCallTreeLogContext(this, trace, null)); + + final List snaps = trace.getTimeManager() + .getAllSnapshots() + .stream() + .filter(s -> (s.getEventThread() == thread) && (s.getKey() >= 0)) + .toList(); + + if (snaps.isEmpty()) { + treeLogModel.log(trace, "No snapshots found in trace for current thread", 0, null, + null); + return null; + } + + treeLogModel.resolve(trace, 0, null); + + final AbstractTraceCallTreeNode rootNode = + new TraceCallTreeCallNode("Start of Trace", "", snaps.getFirst(), List.of(), null); + + final Deque callStack = new LinkedList<>(); + callStack.push(rootNode); + monitor.initialize(snaps.size()); + + final Deque fallthroughs = new LinkedList<>(); + + for (final TraceSnapshot snapshot : snaps) { + monitor.increment(); + + final Address dynamicPC = getDynamicPcFromRegister(coords, snapshot, monitor); + if (dynamicPC == null) { + continue; + } + + final ProgramLocation sloc = + getStaticAddressFromDynamicPC(coords, snapshot, monitor, dynamicPC); + if (sloc == null) { + continue; + } + + final Address pc = sloc.getAddress(); + final Program program = sloc.getProgram(); + + final Function func = + getFunctionForStaticLocation(coords, snapshot, monitor, dynamicPC, sloc); + if (func == null) { + continue; + } + + checkForFallThroughs(monitor, rootNode, callStack, fallthroughs, pc); + + final Instruction inst = program.getListing().getInstructionAt(pc); + if (inst == null) { + treeLogModel.log(trace, "No instruction found", snapshot.getKey(), pc, dynamicPC); + continue; + } + + checkForStartOfFunctions(monitor, trace, thread, rootNode, callStack, snapshot, + dynamicPC, pc, func); + + checkForCalls(monitor, trace, rootNode, callStack, fallthroughs, dynamicPC, snapshot, + inst); + + checkForReturns(monitor, trace, thread, callStack, snapshot, dynamicPC, func, inst); + + treeLogModel.resolve(trace, snapshot.getKey(), dynamicPC); + } + + return rootNode; + } + + /** + * Get values bytes for each parameter of this function + * + * @param trace Trace we are working with + * @param func Current function we are in + * @param thread Thread we are working with + * @param snapshot Current snapshot in the trace + * @param monitor TaskMonitor to check for cancellation + * @return List of bytes for given all parameters + * @throws CancelledException Task is canceled + */ + private List gatherParameters(Trace trace, Function func, TraceThread thread, + TraceSnapshot snapshot, TaskMonitor monitor) throws CancelledException { + final List retMap = new LinkedList<>(); + + for (final Parameter parameter : func.getParameters()) { + monitor.checkCancelled(); + final byte[] bytes = getBytesForVariable(parameter, trace, thread, snapshot); + retMap.add(new ParamNameToBytes(parameter.getName(), bytes)); + } + + return retMap; + } + + /** + * Get values bytes for the return of this function + * + * @param trace Trace we are working with + * @param func Current function we are in + * @param thread Thread we are working with + * @param snapshot Current snapshot in the trace + * @param monitor TaskMonitor to check for cancellation + * @return Bytes for return value + * @throws CancelledException Task is canceled + */ + private byte[] gatherReturn(Trace trace, Function func, TraceThread thread, + TraceSnapshot snapshot, TaskMonitor monitor) throws CancelledException { + monitor.checkCancelled(); + final Parameter parameter = func.getReturn(); + + if ((parameter != null) && (parameter.getMinAddress() != null)) { + return getBytesForVariable(parameter, trace, thread, snapshot); + } + return null; + } + + private void generateCallTreeAndUpdateModel(DebuggerCoordinates coords) { + if ((progressService == null) || (coords == null)) { + return; + } + if ((callTreeNodeCompletableFuture != null) && !callTreeNodeCompletableFuture.isDone()) { + callTreeNodeCompletableFuture.cancel(true); + } + updateModel(null); + treeTableModel.setTableSortState(TableSortState.createUnsortedSortState()); + treeTable.setStatusMessage("Updating..."); + callTreeNodeCompletableFuture = + progressService.execute(true, true, true, m -> createCallTree(m, coords)); + callTreeNodeCompletableFuture.whenComplete((result, ex) -> { + if ((ex != null) && !(ex instanceof CancellationException)) { + treeTable.setStatusMessage(ex.getMessage()); + } + else if (result != null) { + currentRootNode = result; + rootNodeChanged = true; + final Map threadMap = + rootNodesForTraceThreadMap + .computeIfAbsent(coords.getTrace(), e -> new RootCache()) + .rootNodesPerThread(); + threadMap.put(coords.getThread(), result); + refresh(coords); + } + treeTable.setStatusMessage(null); + }); + } + + @Override + public ActionContext getActionContext(MouseEvent event) { + if (myActionContext == null) { + return super.getActionContext(event); + } + return myActionContext; + } + + /** + * Get value bytes for a given variable + * + * @param var Variable to get bytes for + * @param trace Trace we are working with + * @param thread Thread we are working with + * @param snapshot Current snapshot in the trace + * @return Bytes for given variable + */ + private byte[] getBytesForVariable(Variable var, Trace trace, TraceThread thread, + TraceSnapshot snapshot) { + Address address = var.getMinAddress(); + final ByteBuffer buf = ByteBuffer.allocate(var.getLength()); + byte[] bytes; + + if (address.isStackAddress()) { + final CompilerSpec spec = trace.getBaseCompilerSpec(); + final RegisterValue value = trace.getMemoryManager() + .getMemoryRegisterSpace(thread, false) + .getValue(snapshot.getKey(), spec.getStackPointer()); + + final Address stackPointer = + spec.getStackBaseSpace().getAddress(value.getUnsignedValue().longValue()); + + address = stackPointer.add(address.getOffset()); + trace.getMemoryManager().getBytes(snapshot.getKey(), address, buf); + bytes = buf.array(); + } + else if (address.isMemoryAddress()) { + trace.getMemoryManager().getBytes(snapshot.getKey(), address, buf); + bytes = buf.array(); + } + else if (address.isRegisterAddress()) { + final RegisterValue regVal = trace.getMemoryManager() + .getMemoryRegisterSpace(thread, false) + .getValue(snapshot.getKey(), var.getRegister()); + bytes = regVal.getUnsignedValue().toByteArray(); + } + else { + bytes = new byte[0]; + } + return bytes; + } + + @Override + public JComponent getComponent() { + return component; + } + + /** + * Gets the dynamic program counter for a given snapshot directly from the register holding it + * + * @param coords Current debugger coordinates + * @param snapshot Current snapshot in the trace + * @param monitor TaskMonitor to check for cancellation + * @return Address in the PC register + * @throws CancelledException Task is canceled + */ + private Address getDynamicPcFromRegister(DebuggerCoordinates coords, TraceSnapshot snapshot, + TaskMonitor monitor) throws CancelledException { + monitor.checkCancelled(); + + final Trace trace = coords.getTrace(); + final TraceThread thread = coords.getThread(); + if ((thread == null) || (trace == null)) { + treeLogModel.log(trace, "Trace/Thread is null", snapshot.getKey(), null, null); + return null; + } + final Register programCounter = trace.getBaseLanguage().getProgramCounter(); + + final TraceMemorySpace regs = + trace.getMemoryManager().getMemoryRegisterSpace(thread, 0, false); + final RegisterValue pcRegVal = regs.getValue(snapshot.getKey(), programCounter); + + if (pcRegVal == null) { + treeLogModel.log(trace, "PC is null", snapshot.getKey(), null, null); + + return null; + } + + treeLogModel.resolve(trace, snapshot.getKey(), null); + + return trace.getBaseAddressFactory() + .getDefaultAddressSpace() + .getAddress(pcRegVal.getUnsignedValue().longValue()); + } + + /** + * Get the function from the static program associated with this dynamic PC + * + * @param coords Current debugger coordinates + * @param snapshot Current snapshot in the trace + * @param monitor TaskMonitor to check for cancellation + * @param dynamicPC Current dynamic program counter + * @param sloc Current static program location + * @return Static function associated with this dynamic address + * @throws CancelledException Task is canceled + */ + private Function getFunctionForStaticLocation(DebuggerCoordinates coords, + TraceSnapshot snapshot, TaskMonitor monitor, Address dynamicPC, ProgramLocation sloc) + throws CancelledException { + monitor.checkCancelled(); + + final Trace trace = coords.getTrace(); + + final Address pc = sloc.getAddress(); + final Program program = sloc.getProgram(); + + final Function func = program.getListing().getFunctionContaining(pc); + if (func == null) { + treeLogModel.log(trace, + "0x%x in %s is not part of a function".formatted(pc.getOffset(), program.getName()), + snapshot.getKey(), pc, dynamicPC); + + return null; + } + return func; + } + + @Override + public List getPopupActions(Tool t, ActionContext context) { + if ((context != myActionContext) || (context == null) || (listingService == null)) { + return List.of(); + } + + final List selected = myActionContext.getSelected(); + final boolean multipleSelected = selected.size() > 1; + + final List result = new ArrayList<>(); + + if (!multipleSelected) { + // Add go to snap action + String name = "Go To Snapshot %d".formatted(selected.getFirst().getSnapshotKey()); + result.add( + TraceCallTreePopupAction.builder(this, TraceCallTreePopupAction.GROUP1, "", name) + .onAction(ctx -> { + if (traceManager != null) { + traceManager.activateSnap(selected.getFirst().getSnapshotKey()); + } + }) + .build()); + + // Parameter actions + int i = 1; + for (final ParamNameToBytes paramNameToBytes : selected.getFirst().getParameters()) { + final Address address = + AddressEvaluator.evaluate(currentCoords.getTrace().getProgramView(), + NumericUtilities.convertBytesToString(paramNameToBytes.bytes())); + if (address == null) { + continue; + } + + name = "%s @ %s".formatted(paramNameToBytes.name(), address); + + createActionsToViewVariableInListings(t, result, name, Integer.toString(i), + address); + i++; + } + + // Return actions + if (selected.getFirst().getReturnVal() != null) { + final Address address = + AddressEvaluator.evaluate(currentCoords.getTrace().getProgramView(), + NumericUtilities.convertBytesToString(selected.getFirst().getReturnVal())); + if (address != null) { + + name = "Return @ %s".formatted(address); + + createActionsToViewVariableInListings(t, result, name, "", address); + } + } + } + + // Add expand and collapse actions + result.add(TraceCallTreePopupAction + .builder(this, TraceCallTreePopupAction.GROUP2, "2", "Expand Recursively") + .onAction(ctx -> { + selected.forEach(n -> n.setExpanded(true)); + selected.forEach(n -> n.forEachDescendant(e -> e.setExpanded(true))); + updateModel(currentRootNode); + }) + .build()); + + result.add(TraceCallTreePopupAction + .builder(this, TraceCallTreePopupAction.GROUP2, "2", "Collapse Recursively") + .onAction(ctx -> { + selected.forEach(n -> n.setExpanded(false)); + selected.forEach(n -> n.forEachDescendant(e -> e.setExpanded(false))); + updateModel(currentRootNode); + }) + .build()); + + result.add( + TraceCallTreePopupAction.builder(this, TraceCallTreePopupAction.GROUP2, "1", "Expand") + .onAction(ctx -> { + selected.forEach(n -> n.setExpanded(true)); + updateModel(currentRootNode); + }) + .build()); + + result.add( + TraceCallTreePopupAction.builder(this, TraceCallTreePopupAction.GROUP2, "1", "Collapse") + .onAction(ctx -> { + selected.forEach(n -> n.setExpanded(false)); + updateModel(currentRootNode); + }) + .build()); + + return result; + } + + /** + * Get the static address/program associated with this dynamic PC + * + * @param coords Current debugger coordinates + * @param snapshot Current snapshot in the trace + * @param monitor TaskMonitor to check for cancellation + * @param dynamicPC Current dynamic program counter + * @return Associated static ProgramLocation + * @throws CancelledException Task is canceled + */ + private ProgramLocation getStaticAddressFromDynamicPC(DebuggerCoordinates coords, + TraceSnapshot snapshot, TaskMonitor monitor, Address dynamicPC) + throws CancelledException { + monitor.checkCancelled(); + + final Trace trace = coords.getTrace(); + final TraceModuleManager modMan = trace.getModuleManager(); + + final TraceLocation dloc = + new DefaultTraceLocation(trace, null, Lifespan.at(snapshot.getKey()), dynamicPC); + final ProgramLocation sloc = mappingService.getOpenMappedLocation(dloc); + + final Collection intersecting = + modMan.getModulesAt(snapshot.getKey(), dynamicPC); + + if (sloc == null) { + + if (intersecting.isEmpty()) { + treeLogModel.log(trace, "Can't map 0x%x".formatted(dynamicPC.getOffset()), + snapshot.getKey(), null, dynamicPC); + } + else { + final TraceModule module = intersecting.stream().findFirst().get(); + treeLogModel.log(trace, "Module %s is not mapped".formatted( + module.getName(snapshot.getKey())), + snapshot.getKey(), null, dynamicPC); + + } + return null; + } + + return sloc; + } + + private DomainObjectListener makeProgramDomainObjectListener() { + return new DomainObjectListenerBuilder(this).ignoreWhen(() -> !isVisible()) + .any(FUNCTION_ADDED, FUNCTION_REMOVED, FUNCTION_CHANGED, SYMBOL_ADDED, + SYMBOL_PRIMARY_STATE_CHANGED, SYMBOL_RENAMED, EXTERNAL_NAME_ADDED, + EXTERNAL_NAME_REMOVED, EXTERNAL_NAME_CHANGED) + .call(this::changeThatMakesTreeStale) + .build(); + } + + public void programClosed(Program program) { + program.removeListener(domainObjectListener); + } + + public void programOpened(Program program) { + program.addListener(domainObjectListener); + } + + private void refresh(DebuggerCoordinates coords) { + final Address pcAddress = PCLocationTrackingSpec.INSTANCE.computeTraceAddress(tool, coords); + final Function func = DebuggerStaticMappingUtils.getFunction(pcAddress, coords, tool); + if (pcAddress == null) { + treeLogModel.log(coords.getTrace(), "Unable to resolve PC", coords.getSnap(), null, + null); + treeTable.setStatusMessage("Location unknown, check debug console"); + updateModel(currentRootNode); + return; + } + + if (func == null) { + treeLogModel.log(coords.getTrace(), "Can't map 0x%x".formatted(pcAddress.getOffset()), + coords.getSnap(), null, pcAddress); + treeTable.setStatusMessage("Location unknown, check debug console"); + updateModel(currentRootNode); + return; + } + final List list = currentRootNode.find(n -> { + if (n instanceof final AbstractTraceCallTreeNode TraceCallTreeNode) { + return TraceCallTreeNode.getName().equals(func.getName()) && + (coords.getSnap() >= TraceCallTreeNode.getSnapshotKey()); + } + treeTable.setStatusMessage("Can't find node for current location, check debug console"); + return false; + }); + currentNode = (AbstractTraceCallTreeNode) list.getLast(); + currentNode.forEachAncestor(n -> n.setExpanded(true)); + scrollToCurrentNode = true; + updateModel(currentRootNode); + treeTable.setStatusMessage(null); + } + + public void setCoordinates(DebuggerCoordinates coords) { + if (currentCoords == coords) { + return; + } + if ((currentCoords != null) && (currentCoords.getTrace() != null)) { + currentCoords.getTrace().removeListener(traceCallTreeEventListener); + } + scrollToCurrentNode = true; + currentCoords = coords; + if ((currentCoords == DebuggerCoordinates.NOWHERE) || (currentCoords == null) || + (currentCoords.getTrace() == null)) { + treeTable.setStatusMessage(null); + updateModel(null); + return; + } + + currentCoords.getTrace().addListener(traceCallTreeEventListener); + + if (rootNodesForTraceThreadMap.containsKey(coords.getTrace())) { + final Map threadMap = + rootNodesForTraceThreadMap.get(coords.getTrace()).rootNodesPerThread(); + if (threadMap.containsKey(coords.getThread())) { + rootNodeChanged = threadMap.get(coords.getThread()) != currentRootNode; + currentRootNode = threadMap.get(coords.getThread()); + refresh(coords); + } + else { + generateCallTreeAndUpdateModel(coords); + } + } + else { + generateCallTreeAndUpdateModel(coords); + } + } + + @AutoServiceConsumed + private void setMappingService(DebuggerStaticMappingService mappingService) { + if (this.mappingService != null) { + this.mappingService.removeChangeListener(listener); + } + this.mappingService = mappingService; + if (this.mappingService != null) { + this.mappingService.addChangeListener(listener); + } + } + + public void traceClosed(Trace trace) { + final RootCache map = rootNodesForTraceThreadMap.remove(trace); + if (map != null) { + map.rootNodesPerThread().clear(); + } + + } + + private void updateModel(AbstractTraceCallTreeNode node) { + SwingUtilities.invokeLater(() -> { + treeTableModel.setRootNode(node); + if (rootNodeChanged) { + rootNodeChanged = false; + if (node != null) { + treeTableModel.setNumberOfParameterColumns(node.getLargestParamSize()); + } + } + }); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/tracecalltree/TraceCallTreeReturnNode.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/tracecalltree/TraceCallTreeReturnNode.java new file mode 100644 index 0000000000..9a1c9e4f80 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/tracecalltree/TraceCallTreeReturnNode.java @@ -0,0 +1,34 @@ +/* ### + * 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.tracecalltree; + +import java.util.List; + +import ghidra.trace.model.time.TraceSnapshot; +import resources.Icons; + +public class TraceCallTreeReturnNode extends AbstractTraceCallTreeNode { + public TraceCallTreeReturnNode(String name, String module, TraceSnapshot snap, + List parameters, byte[] returnVal) { + super(name, module, snap, parameters, returnVal); + icon = Icons.ARROW_UP_LEFT_ICON; + } + + @Override + public String getTreeData() { + return "Return: %s".formatted(name); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/tracecalltree/TraceCallTreeTable.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/tracecalltree/TraceCallTreeTable.java new file mode 100644 index 0000000000..fedba72f25 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/tracecalltree/TraceCallTreeTable.java @@ -0,0 +1,44 @@ +/* ### + * 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.tracecalltree; + +import javax.swing.*; + +import docking.util.MessageOverlay; +import docking.widgets.gtreetable.GTreeTable; +import docking.widgets.gtreetable.GTreeTableModel; +import docking.widgets.table.GTable; + +public class TraceCallTreeTable extends GTreeTable { + private MessageOverlay msgOverlay; + + public TraceCallTreeTable(GTreeTableModel model) { + super(model); + } + + @Override + protected void addTableToPanel(GTable table) { + msgOverlay = new MessageOverlay(); + final JScrollPane pane = new JScrollPane(table); + final JLayer layer = new JLayer<>(pane, msgOverlay); + add(layer); + } + + public void setStatusMessage(String msg) { + msgOverlay.setMessage(msg); + repaint(); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/tracecalltree/TraceCallTreeTailCallNode.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/tracecalltree/TraceCallTreeTailCallNode.java new file mode 100644 index 0000000000..9c30d6e77d --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/tracecalltree/TraceCallTreeTailCallNode.java @@ -0,0 +1,34 @@ +/* ### + * 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.tracecalltree; + +import java.util.List; + +import ghidra.trace.model.time.TraceSnapshot; +import resources.Icons; + +public class TraceCallTreeTailCallNode extends AbstractTraceCallTreeNode { + public TraceCallTreeTailCallNode(String name, String module, TraceSnapshot snap, + List parameters, byte[] returnVal) { + super(name, module, snap, parameters, returnVal); + icon = Icons.NAVIGATE_ON_INCOMING_EVENT_ICON; + } + + @Override + public String getTreeData() { + return "Tail Call: %s".formatted(name); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java index 0220075812..44aaf11429 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java @@ -16,6 +16,7 @@ package ghidra.app.plugin.core.debug.service.modules; import java.io.FileNotFoundException; +import java.io.IOException; import java.net.URL; import java.util.*; import java.util.concurrent.*; @@ -30,6 +31,8 @@ import ghidra.app.plugin.core.debug.DebuggerPluginPackage; import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent; import ghidra.app.plugin.core.debug.event.TraceOpenedPluginEvent; import ghidra.app.plugin.core.debug.service.modules.DebuggerStaticMappingContext.ChangeCollector; +import ghidra.app.plugin.core.debug.gui.DebuggerResources; +import ghidra.app.plugin.core.debug.gui.action.ByModuleAutoMapSpec; import ghidra.app.plugin.core.debug.service.modules.DebuggerStaticMappingProposals.*; import ghidra.app.plugin.core.debug.utils.ProgramURLUtils; import ghidra.app.services.*; @@ -50,8 +53,11 @@ import ghidra.trace.model.*; import ghidra.trace.model.memory.TraceMemoryRegion; import ghidra.trace.model.modules.*; import ghidra.trace.model.program.TraceProgramView; +import ghidra.util.HTMLUtilities; import ghidra.util.Msg; import ghidra.util.exception.CancelledException; +import ghidra.util.exception.VersionException; +import ghidra.util.task.Task; import ghidra.util.task.TaskMonitor; @PluginInfo( @@ -79,6 +85,8 @@ public class DebuggerStaticMappingServicePlugin extends Plugin private DebuggerTraceManagerService traceManager; @AutoServiceConsumed private ProgramManager programManager; + @AutoServiceConsumed + private ProgressService progressService; @SuppressWarnings("unused") private final AutoService.Wiring autoWiring; @@ -119,16 +127,127 @@ public class DebuggerStaticMappingServicePlugin extends Plugin }, executor); } + private void checkTraceMapping(Trace trace, TaskMonitor monitor) throws CancelledException { + DebuggerAutoMappingService autoMappingService = + tool.getService(DebuggerAutoMappingService.class); + if (!(autoMappingService.getAutoMapSpec() instanceof ByModuleAutoMapSpec)) { + // TODO GP-6856: This should be factored into the ByModuleAutoMapSpec and the + // other specs should have their own implementation + return; + } + + final TraceModuleManager moduleManager = trace.getModuleManager(); + final DebuggerConsoleService consoleService = tool.getService(DebuggerConsoleService.class); + + for (final TraceModule module : moduleManager.getAllModules()) { + for (final Lifespan lifespan : module.getObject().getLife().spans()) { + final Long firstSnap = lifespan.min(); + + final DomainFile match = findBestModuleProgram( + module.getMaxAddress(firstSnap).getAddressSpace(), module, firstSnap); + if (match == null) { + consoleService.log(DebuggerResources.ICON_LOG_ERROR, + "The module " + + HTMLUtilities.escapeHTML(module.getName(firstSnap)) + + " was not found in the project", + new DebuggerMissingModuleActionContext(module, firstSnap)); + } + else { + if (match.isOpen()) { + try { + if (match.getDomainObject(this, false, false, + monitor) instanceof final Program p) { + if (!alreadyMapped(trace, firstSnap, p)) { + final ModuleMapProposal proposeModuleMap = + proposeModuleMap(module, firstSnap, p); + final Map computeMap = + proposeModuleMap.computeMap(); + addMappings(computeMap.values(), monitor, true, ""); + } + } + } + catch (VersionException | IOException e) { + // This should not happen since the domain file is already open + Msg.error(this, + "Error getting domainObject: %s".formatted(e.getMessage())); + } + } + else { + consoleService.log(DebuggerResources.ICON_MODULES, + "Program " + + HTMLUtilities.escapeHTML(match.getPathname()) + + " is used by " + trace.getName() + + ". Open it so it can mapped.", + new DebuggerOpenProgramActionContext(match)); + } + } + } + } + } + + private void checkAllTraceMappingsForProgram(Program program, TaskMonitor monitor) + throws CancelledException { + Iterator iterator = + traceManager.getOpenTraces().stream().filter(t -> !t.isClosed()).iterator(); + DebuggerAutoMappingService autoMappingService = + tool.getService(DebuggerAutoMappingService.class); + + while (iterator.hasNext()) { + Trace trace = iterator.next(); + if (!(autoMappingService.getAutoMapSpec() instanceof ByModuleAutoMapSpec)) { + // TODO GP-6856: This should be factored into the ByModuleAutoMapSpec and the + // other specs should have their own implementation + continue; + } + final TraceModuleManager moduleManager = trace.getModuleManager(); + + for (final TraceModule module : moduleManager.getAllModules()) { + for (final Lifespan lifespan : module.getObject().getLife().spans()) { + final Long firstSnap = lifespan.min(); + if (alreadyMapped(trace, firstSnap, program)) { + continue; + } + + final DomainFile match = findBestModuleProgram( + module.getMaxAddress(firstSnap).getAddressSpace(), module, firstSnap); + if (match != null && match.equals(program.getDomainFile())) { + final ModuleMapProposal proposeModuleMap = + proposeModuleMap(module, firstSnap, program); + final Map computeMap = + proposeModuleMap.computeMap(); + addMappings(computeMap.values(), monitor, true, ""); + } + } + } + } + } + + private boolean alreadyMapped(Trace trace, long snap, Program program) { + return getOpenMappedProgramsAtSnap(trace, snap).contains(program); + } + @Override public void processEvent(PluginEvent event) { - if (event instanceof ProgramOpenedPluginEvent) { + if (event instanceof ProgramOpenedPluginEvent ev) { CompletableFuture.runAsync(this::programsChanged, executor); + progressService.execute(new Task("Check trace mappings") { + @Override + public void run(TaskMonitor monitor) throws CancelledException { + checkAllTraceMappingsForProgram(ev.getProgram(), monitor); + } + }); } else if (event instanceof ProgramClosedPluginEvent) { CompletableFuture.runAsync(this::programsChanged, executor); } - else if (event instanceof TraceOpenedPluginEvent) { + else if (event instanceof TraceOpenedPluginEvent ev) { CompletableFuture.runAsync(this::tracesChanged, executor); + progressService.execute(new Task("Check trace mappings") { + @Override + public void run(TaskMonitor monitor) throws CancelledException { + checkTraceMapping(ev.getTrace(), monitor); + } + }); } else if (event instanceof TraceClosedPluginEvent) { CompletableFuture.runAsync(this::tracesChanged, executor); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DynamicStaticSynchronizationPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DynamicStaticSynchronizationPlugin.java index ba5a5b7350..a1d3a929f8 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DynamicStaticSynchronizationPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DynamicStaticSynchronizationPlugin.java @@ -703,7 +703,7 @@ public class DynamicStaticSynchronizationPlugin extends Plugin { consoleService.log(DebuggerResources.ICON_LOG_ERROR, "The module " + HTMLUtilities.escapeHTML(mod.getName(snap)) + " was not found in the project", - new DebuggerMissingModuleActionContext(mod)); + new DebuggerMissingModuleActionContext(mod, snap)); } /** * Once the programs are opened, including those which are successfully imported, the diff --git a/Ghidra/Framework/Docking/data/docking.theme.properties b/Ghidra/Framework/Docking/data/docking.theme.properties index a883784345..2be8ab6e91 100644 --- a/Ghidra/Framework/Docking/data/docking.theme.properties +++ b/Ghidra/Framework/Docking/data/docking.theme.properties @@ -67,6 +67,8 @@ color.bg.formatted.field.error = color.palette.lightcoral color.bg.formatted.field.editing = color.bg.filterfield color.fg.formatted.field.editing = color.fg.filterfield +color.bg.messageoverlay = color.palette.lightcornflowerblue +color.fg.messageoverlay = color.palette.white icon.folder.new = folder_add.png icon.toggle.expand = expand.gif @@ -189,6 +191,7 @@ font.widget.tabs.list = sansserif-bold-9 font.wizard.border.title = sansserif-plain-10 +font.messageoverlay = SansSerif-BOLDITALIC-24 [Dark Defaults] diff --git a/Ghidra/Framework/Docking/src/main/java/docking/util/MessageOverlay.java b/Ghidra/Framework/Docking/src/main/java/docking/util/MessageOverlay.java new file mode 100644 index 0000000000..4743c425e6 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/util/MessageOverlay.java @@ -0,0 +1,75 @@ +/* ### + * 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.util; + +import java.awt.*; + +import javax.swing.JComponent; +import javax.swing.plaf.LayerUI; + +import generic.theme.GColor; +import generic.theme.Gui; + +public class MessageOverlay extends LayerUI { + private final String FONT_ID = "font.messageoverlay"; + private final Color backgroundColor = new GColor("color.bg.messageoverlay"); + private final Color textColor = new GColor("color.fg.messageoverlay"); + + private String message = null; + + @Override + public void paint(Graphics g, JComponent c) { + super.paint(g, c); + if (message == null) { + return; + } + + final Graphics2D g2 = (Graphics2D) g.create(); + + g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .60f)); + + final Font font = Gui.getFont(FONT_ID); + g2.setFont(font); + + FontMetrics fm = g2.getFontMetrics(font); + + final int bottomY = (int) (c.getY() + c.getHeight()); + + final int startX = 0; + final int startY = bottomY - 5; + final Color[] colors = { backgroundColor, new Color(0, 0, 0, 0) }; + + final int backgroundHeight = (fm.getHeight() * 3); + final int backgroundWidth = c.getWidth(); + final int backgroundY = bottomY - backgroundHeight; + + final float[] fractions = { 0.0f, .95f }; + final int upperY = backgroundY; + + g2.setPaint(new LinearGradientPaint(new Point(startX, startY), new Point(startX, upperY), + fractions, colors)); + g2.fillRect(startX, backgroundY, backgroundWidth, backgroundHeight); + + g2.setPaint(textColor); + + final int centerX = (c.getWidth() - fm.stringWidth(message)) / 2; + g2.drawString(message, centerX, startY); + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/gtreetable/GTreeTable.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/gtreetable/GTreeTable.java new file mode 100644 index 0000000000..41cec5da76 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/gtreetable/GTreeTable.java @@ -0,0 +1,104 @@ +/* ### + * 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.gtreetable; + +import java.awt.BorderLayout; +import java.awt.Rectangle; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +import javax.swing.JPanel; +import javax.swing.JScrollPane; + +import docking.widgets.table.GTable; +import docking.widgets.table.GTableFilterPanel; + +public class GTreeTable extends JPanel { + private final GTable table; + private final GTableFilterPanel filterPanel; + + private final GTreeTableModel tableModel; + + public GTreeTable(GTreeTableModel model) { + tableModel = model; + table = new GTable(tableModel); + setLayout(new BorderLayout()); + + addTableToPanel(table); + + table.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + final int row = table.rowAtPoint(e.getPoint()); + final int col = table.columnAtPoint(e.getPoint()); + final GTreeTableNode selected = filterPanel.getSelectedItem(); + if (selected == null) { + return; + } + if (table.getCellRenderer(row, + col) instanceof final GTreeTableCellRenderer renderer) { + final Rectangle cellRect = table.getCellRect(row, col, false); + + if (renderer.inExpandIcon(selected, e.getX() - cellRect.x)) { + selected.setExpanded(!selected.isExpanded()); + tableModel.reload(); + } + } + } + }); + + filterPanel = new GTableFilterPanel<>(table, tableModel); + add(filterPanel, BorderLayout.SOUTH); + } + + /** + * Sort of a hack to allow extending classes the ability to add any wrappers around the GTable + * if desired + * + * @param table + * Table to add to the panel + */ + protected void addTableToPanel(GTable table) { + add(new JScrollPane(table)); + } + + /** + * Get the filter panel of this table + * + * @return The filter panel + */ + public GTableFilterPanel getFilterPanel() { + return filterPanel; + } + + /** + * Get the GTable of this GTreeTable + * + * @return The table + */ + public GTable getTable() { + return table; + } + + /** + * Get the model tied to this GTreeTable + * + * @return The model + */ + public GTreeTableModel getTableModel() { + return tableModel; + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/gtreetable/GTreeTableCellRenderer.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/gtreetable/GTreeTableCellRenderer.java new file mode 100644 index 0000000000..ded11f9672 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/gtreetable/GTreeTableCellRenderer.java @@ -0,0 +1,115 @@ +/* ### + * 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.gtreetable; + +import java.awt.*; + +import javax.swing.*; +import javax.swing.border.Border; + +import docking.widgets.table.GTableCellRenderer; +import docking.widgets.table.GTableCellRenderingData; +import ghidra.docking.settings.Settings; +import ghidra.util.table.column.GColumnRenderer; + +public class GTreeTableCellRenderer extends GTableCellRenderer + implements GColumnRenderer { + private class ExpandCollapseBorder implements Border { + GTreeTableNode node; + + ExpandCollapseBorder(GTreeTableNode node) { + super(); + this.node = node; + } + + @Override + public Insets getBorderInsets(final Component c) { + return insets; + } + + @Override + public boolean isBorderOpaque() { + return false; + } + + @Override + public void paintBorder(final Component c, final Graphics g, final int x, final int y, + final int width, final int height) { + if (node.hasVisibleChildren()) { + final JLabel renderer = new JLabel("", SwingConstants.RIGHT); + renderer.setIcon(node.isExpanded() ? expandedIcon : collapsedIcon); + renderer.setSize(insets.left, height); + renderer.paint(g); + } + } + } + + private static final int PIXELS_PER_LEVEL = 16; + private final Insets insets = new Insets(0, 0, 0, 0); + private final Icon expandedIcon; + private final Icon collapsedIcon; + private final int maxIconWidth; + + public GTreeTableCellRenderer() { + expandedIcon = UIManager.getIcon("Tree.expandedIcon"); + collapsedIcon = UIManager.getIcon("Tree.collapsedIcon"); + final int expandedWidth = expandedIcon == null ? 0 : expandedIcon.getIconWidth(); + final int collapsedWidth = collapsedIcon == null ? 0 : collapsedIcon.getIconWidth(); + maxIconWidth = Math.max(expandedWidth, collapsedWidth); + } + + @Override + public String getFilterString(GTreeTableNode t, Settings settings) { + return t.getTreeData(); + } + + @Override + public Component getTableCellRendererComponent(GTableCellRenderingData data) { + final JLabel renderer = (JLabel) super.getTableCellRendererComponent(data); + + if (data.getRowObject() instanceof final GTreeTableNode c) { + final boolean selected = data.isSelected(); + final JTable table = data.getTable(); + + renderer.setForeground( + selected ? table.getSelectionForeground() : table.getForeground()); + renderer.setBackground(selected ? table.getSelectionBackground() + : getAlternatingBackgroundColor(table, data.getRowViewIndex())); + renderer.setFont(table.getFont()); + renderer.setToolTipText(c.getName()); + insets.left = maxIconWidth + (c.getLevel() * PIXELS_PER_LEVEL); + renderer.setIcon(c.getIcon()); + renderer.setText(c.getTreeData()); + renderer.setBorder(new ExpandCollapseBorder(c)); + } + return renderer; + + } + + /** + * Check if x position is in the expand icon space + * + * @param node + * Node to determine how indented the expand icon is + * @param x + * X coordinate to check against + * @return true/false if X coordinate is in expand icon space + */ + public boolean inExpandIcon(GTreeTableNode node, int x) { + final int iconStart = (node.getLevel() * PIXELS_PER_LEVEL); + return ((x > iconStart) && (x < (iconStart + maxIconWidth))); + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/gtreetable/GTreeTableModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/gtreetable/GTreeTableModel.java new file mode 100644 index 0000000000..e77d00fb5e --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/gtreetable/GTreeTableModel.java @@ -0,0 +1,153 @@ +/* ### + * 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.gtreetable; + +import java.util.*; + +import docking.widgets.table.*; +import docking.widgets.table.threaded.ThreadedTableModelStub; +import ghidra.docking.settings.Settings; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.util.datastruct.Accumulator; +import ghidra.util.exception.CancelledException; +import ghidra.util.table.column.GColumnRenderer; +import ghidra.util.task.TaskMonitor; + +public class GTreeTableModel extends ThreadedTableModelStub { + protected class TreeColumn extends AbstractDynamicTableColumnStub { + private final String columnName; + private final GTreeTableCellRenderer renderer; + + public TreeColumn(String columnName) { + this.columnName = columnName; + renderer = new GTreeTableCellRenderer<>(); + } + + @Override + public String getColumnName() { + return columnName; + } + + @Override + public GColumnRenderer getColumnRenderer() { + return renderer; + } + + @Override + public GTreeTableNode getValue(T rowObject, Settings settings, ServiceProvider provider) + throws IllegalArgumentException { + return rowObject; + } + } + + private GTreeTableNode rootNode; + + public GTreeTableModel(GTreeTableNode rootNode) { + super("", null); + this.rootNode = rootNode; + } + + /** + * Add the {@TreeColumn} by default, override to add additional columns + *

+ * {@inheritDoc} + */ + @Override + protected TableColumnDescriptor createTableColumnDescriptor() { + final TableColumnDescriptor descriptor = new TableColumnDescriptor<>(); + descriptor.addVisibleColumn(new TreeColumn(treeColumnName())); + + return descriptor; + } + + /** + * Any rows that match the filter have all of their ancestors added to the filtered tree to + * ensure the tree structure remains intact + *

+ * {@inheritDoc} + */ + @Override + protected List doFilter(List data, TableSortingContext lastSortingContext, + TaskMonitor monitor) throws CancelledException { + if ((data.isEmpty()) || !hasFilter()) { + return data; + } + + final TableFilter filterCopy = getTableFilter(); + final Set filteredList = new HashSet<>(); + final List returnFilteredList = new ArrayList<>(); + + monitor.initialize(data.size()); + + for (final T rowObject : data) { + if (monitor.isCancelled()) { + return new ArrayList<>(filteredList); + } + + if (filterCopy.acceptsRow(rowObject)) { + filteredList.add(rowObject); + rowObject.forEachAncestor(c -> filteredList.add((T) c)); + } + monitor.incrementProgress(1); + } + + // Reorder them + for (final T rowObject : data) { + if (monitor.isCancelled()) { + return new ArrayList<>(filteredList); + } + + if (filteredList.contains(rowObject)) { + returnFilteredList.add(rowObject); + } + } + + return returnFilteredList; + } + + @Override + protected void doLoad(Accumulator accumulator, TaskMonitor monitor) + throws CancelledException { + if (rootNode != null) { + for (final GTreeTableNode c : rootNode.expandedDescendants()) { + monitor.checkCancelled(); + if (c.isVisible()) { + accumulator.add((T) c); + } + } + } + } + + /** + * Set the root node of this model + * + * @param node + * Root node to set + */ + public void setRootNode(GTreeTableNode node) { + rootNode = node; + reload(); + } + + /** + * Name of the tree column, allows extending classes the ability to change the name + * + * @return Tree column name + */ + protected String treeColumnName() { + return "Tree"; + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/gtreetable/GTreeTableNode.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/gtreetable/GTreeTableNode.java new file mode 100644 index 0000000000..fdedf4f7b7 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/gtreetable/GTreeTableNode.java @@ -0,0 +1,491 @@ +/* ### + * 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.gtreetable; + +import java.io.Serializable; +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import javax.swing.Icon; + +public class GTreeTableNode implements Serializable { + private class EachAncestorIterator implements Iterator { + GTreeTableNode curNode = getParent(); + + @Override + public boolean hasNext() { + return curNode != null; + } + + @Override + public GTreeTableNode next() { + final GTreeTableNode ret = curNode; + curNode = curNode.getParent(); + return ret; + } + } + + private class EachDecendantIterator implements Iterator { + final List nodes = new ArrayList<>(children); + + @Override + public boolean hasNext() { + return !nodes.isEmpty(); + } + + @Override + public GTreeTableNode next() { + final GTreeTableNode curNode = nodes.removeFirst(); + nodes.addAll(curNode.getChildren()); + return curNode; + } + + } + + private class EachExpandedIterator implements Iterator { + final List nodes = new ArrayList<>(children); + + @Override + public boolean hasNext() { + return !nodes.isEmpty(); + } + + @Override + public GTreeTableNode next() { + final GTreeTableNode curNode = nodes.removeFirst(); + if (curNode.isExpanded()) { + nodes.addAll((curNode.getChildren())); + } + return curNode; + } + + } + + protected final String name; + protected Icon icon; + protected boolean expanded; + + protected GTreeTableNode parent; + + protected List children; + + protected boolean visible; + + public GTreeTableNode(final String name) { + this.name = name; + expanded = false; + children = Collections.synchronizedList(new ArrayList<>()); + visible = true; + } + + /** + * Add new child to this node's children + * + * @param newChild + * New child to add + */ + public void add(GTreeTableNode newChild) { + if ((newChild != null) && (newChild.getParent() == this)) { + insert(newChild, getChildCount() - 1); + } + else { + insert(newChild, getChildCount()); + } + } + + /** + * Get an iterable of all the ancestors of this node + * + * @return An iterable of ancestors + */ + public Iterable ancestors() { + return EachAncestorIterator::new; + } + + /** + * Get a list of all descendants of this node in depth first search order + * + * @return List of descendants in DFS order + */ + public List depthFirstSearchList() { + final List nodes = new LinkedList<>(); + final List result = new LinkedList<>(); + + result.add(this); + nodes.addAll(0, getChildren()); + + while (!nodes.isEmpty()) { + final GTreeTableNode curNode = nodes.removeFirst(); + result.add(curNode); + nodes.addAll(0, curNode.getChildren()); + } + + return result; + } + + /** + * Get an iterable of all the descendants of this node + * + * @return An iterable of descendants + */ + public Iterable descendants() { + return EachDecendantIterator::new; + } + + /** + * Get an iterable of all the expanded descendants of this node + * + * @return An iterable of expanded descendants + */ + public Iterable expandedDescendants() { + return EachExpandedIterator::new; + } + + /** + * Find nodes in descendants that match a certain condition + * + * @param condition + * Predicate to match nodes on + * @return List of nodes matching the condition + */ + public List find(Predicate condition) { + final List nodes = new LinkedList<>(); + final List result = new LinkedList<>(); + + result.add(this); + nodes.addAll(0, getChildren()); + + while (!nodes.isEmpty()) { + final GTreeTableNode curNode = nodes.removeFirst(); + if (condition.test(curNode)) { + result.add(curNode); + } + nodes.addAll(0, curNode.getChildren()); + } + return result; + } + + /** + * Perform an action on each ancestor of this node + * + * @param action + * To perform on each ancestor + */ + public void forEachAncestor(final Consumer action) { + GTreeTableNode curNode = getParent(); + while (curNode != null) { + action.accept(curNode); + curNode = curNode.getParent(); + } + } + + /** + * Perform an action on each descendant of this node + * + * @param action + * To perform on each descendant + */ + public void forEachDescendant(final Consumer action) { + final List nodes = new ArrayList<>(children); + + while (!nodes.isEmpty()) { + final GTreeTableNode curNode = nodes.removeFirst(); + action.accept(curNode); + nodes.addAll(curNode.getChildren()); + } + } + + /** + * Perform an action on each expanded descendant of this node + * + * @param action + * To perform on each expanded descendant + */ + public void forEachExpanded(final Consumer action) { + final List nodes = new ArrayList<>(children); + + while (!nodes.isEmpty()) { + final GTreeTableNode curNode = nodes.removeFirst(); + action.accept(curNode); + + if (curNode.isExpanded()) { + nodes.addAll((curNode.getChildren())); + } + } + } + + /** + * Get number of children + * + * @return Number of children + */ + public int getChildCount() { + return children.size(); + } + + /** + * Get list of children + * + * @return List of children + */ + public List getChildren() { + return children; + } + + /** + * Get list of descendants who are expanded + * + * @return List of expanded descendants + */ + public List getExpanded() { + final List nodes = new ArrayList<>(); + forEachExpanded(nodes::add); + return nodes; + } + + /** + * Get icon + * + * @return icon + */ + public Icon getIcon() { + return icon; + } + + /** + * Get tree depth from the root node + * + * @return Level from root node + */ + public int getLevel() { + int levels = 0; + GTreeTableNode ancestor = this; + while ((ancestor = ancestor.getParent()) != null) { + ++levels; + } + + return levels; + } + + /** + * Get name of node + * + * @return name + */ + public String getName() { + return name; + } + + /** + * Get parent of node + * + * @return parent + */ + public GTreeTableNode getParent() { + return parent; + } + + /** + * Get list of ancestors in order leading to this node + * + * @return List of nodes leading to this one + */ + public List getPath() { + final List path = new ArrayList<>(); + forEachAncestor(path::add); + return path; + } + + /** + * Get root node + * + * @return root node + */ + public GTreeTableNode getRoot() { + for (GTreeTableNode cur = this;; cur = cur.getParent()) { + if (cur.getParent() == null) { + return cur; + } + } + } + + /** + * Get data associated with this node + * + * @return data + */ + public String getTreeData() { + return name; + } + + /** + * Check if node has children + * + * @return true/false if node has children + */ + public boolean hasChildren() { + return (children != null) && !children.isEmpty(); + } + + /** + * Check if another node is in this node's ancestry, the current node will return true as + * being in its own ancestry + * + * @param anotherNode + * Node to check for in the current nodes ancestry + * @return true/false if other node is in the ancestry + */ + public boolean hasNodeInItsAncestry(GTreeTableNode anotherNode) { + if (anotherNode == null) { + return false; + } + + for (GTreeTableNode cur = this; cur != null; cur = cur.getParent()) { + if (anotherNode == cur) { + return true; + } + } + return false; + } + + /** + * Check if this node has any visible children + * + * @return true/false if any children are visible + */ + public boolean hasVisibleChildren() { + return (children != null) && children.stream().anyMatch(c -> c.visible); + } + + /** + * Insert a new node into the children of this node + * + * @param newChild + * New child to insert + * @param childIndex + * Index to insert child at + */ + public void insert(GTreeTableNode newChild, int childIndex) { + if (hasNodeInItsAncestry(Objects.requireNonNull(newChild))) { + throw new IllegalArgumentException( + "New child is already an ancestor, and cycles are not permitted"); + } + final GTreeTableNode oldParent = newChild.getParent(); + if (oldParent != null) { + oldParent.remove(newChild); + } + + newChild.setParent(this); + if (children == null) { + children = Collections.synchronizedList(new ArrayList<>()); + } + + children.add(childIndex, newChild); + } + + /** + * Check if node is expanded + * + * @return true/false if expanded + */ + public boolean isExpanded() { + return expanded; + } + + /** + * Check if node is leaf node + * + * @return true/false if leaf + */ + public boolean isLeaf() { + return children.isEmpty(); + } + + /** + * Check if node is root node (i.e. it has no parent) + * + * @return true/false if root + */ + public boolean isRoot() { + return getParent() == null; + } + + /** + * @return + */ + public boolean isVisible() { + return visible; + } + + /** + * Remove child from list of children + * + * @param aChild + * Child to remove + */ + public void remove(GTreeTableNode aChild) { + children.remove(aChild); + aChild.setParent(null); + } + + /** + * Remove child from list of children + * + * @param childIndex + * Index of child to remove + */ + public void remove(int childIndex) { + final GTreeTableNode child = children.get(childIndex); + children.remove(childIndex); + child.setParent(null); + } + + /** + * Change node expanded state + * + * @param expanded + * Expanded state + */ + public void setExpanded(boolean expanded) { + this.expanded = expanded; + } + + /** + * Set parent of node + * + * @param newParent + * Parent to set + */ + public void setParent(GTreeTableNode newParent) { + parent = newParent; + } + + /** + * Change node visibility state + * + * @param visible + * Node visibility + */ + public void setVisible(boolean visible) { + this.visible = visible; + } + + @Override + public String toString() { + return name == null ? "" : name; + } +}