mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2026-10-01 17:11:44 -09:00
Merge remote-tracking branch 'origin/GP-5893_ghidra_red_CallTreeViewer'
This commit is contained in:
@@ -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<DebuggerListing> 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();
|
||||
}
|
||||
|
||||
@@ -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<T> future = futureSupplier.apply(monitor);
|
||||
future.handle((t, ex) -> {
|
||||
if (canCancel) {
|
||||
monitor.cancel();
|
||||
}
|
||||
monitor.close();
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> 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 = """
|
||||
<context>
|
||||
<schema name='TenetPlusPlusSession' canonical='yes'>
|
||||
<interface name='Aggregate'/>
|
||||
<interface name='Process'/>
|
||||
<attribute name='Threads' schema='ThreadContainer' />
|
||||
<attribute name='Memory' schema='Memory'/>
|
||||
<attribute name='Breakpoints' schema='BreakpointContainer'/>
|
||||
<attribute name='Modules' schema='ModuleContainer'/>
|
||||
</schema>
|
||||
<schema name='ThreadContainer' canonical='yes'>
|
||||
<element schema='Thread' />
|
||||
</schema>
|
||||
<schema name='Thread'>
|
||||
<interface name='Aggregate'/>
|
||||
<interface name='Thread'/>
|
||||
<interface name='ExecutionStateful'/>
|
||||
<attribute name='Stack' schema='Stack'/>
|
||||
</schema>
|
||||
<schema name='Stack' canonical='yes'>
|
||||
<interface name='Stack'/>
|
||||
<interface name='Aggregate'/>
|
||||
<element schema='Frame'/>
|
||||
</schema>
|
||||
<schema name='Frame'>
|
||||
<interface name='Aggregate'/>
|
||||
<interface name='StackFrame'/>
|
||||
<attribute name='Registers' schema='RegisterContainer'/>
|
||||
</schema>
|
||||
<schema name='RegisterContainer'>
|
||||
<interface name='RegisterContainer'/>
|
||||
<element schema='Register'/>
|
||||
</schema>
|
||||
<schema name='Register'>
|
||||
<interface name='Register'/>
|
||||
</schema>
|
||||
<schema name='Memory' canonical='yes'>
|
||||
<interface name='Memory'/>
|
||||
<element schema='MemoryRegion'/>
|
||||
</schema>
|
||||
<schema name='MemoryRegion'>
|
||||
<interface name='MemoryRegion'/>
|
||||
</schema>
|
||||
<schema name='ModuleContainer' canonical='yes'>
|
||||
<element schema='Module'/>
|
||||
</schema>
|
||||
<schema name='Module'>
|
||||
<interface name='Module'/>
|
||||
<attribute name='Sections' schema='SectionContainer'/>
|
||||
</schema>
|
||||
<schema name='SectionContainer' canonical='yes'>
|
||||
<element schema='Section'/>
|
||||
</schema>
|
||||
<schema name='Section'>
|
||||
<interface name='Section'/>
|
||||
</schema>
|
||||
<schema name='BreakpointContainer' canonical='yes'>
|
||||
<element schema='Breakpoint'/>
|
||||
</schema>
|
||||
<schema name='Breakpoint'>
|
||||
<interface name='BreakpointSpec'/>
|
||||
<interface name='BreakpointLocation'/>
|
||||
</schema>
|
||||
</context>
|
||||
""";
|
||||
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<ModuleEvent> load,
|
||||
List<ModuleEvent> 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<LoadSpec> findSupportedLoadSpecs(final ByteProvider provider)
|
||||
throws IOException {
|
||||
final List<LoadSpec> 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<Option> getDefaultOptions(final ByteProvider provider, final LoadSpec loadSpec,
|
||||
final DomainObject domainObject, final boolean loadIntoProgram,
|
||||
final boolean mirrorFsLayout) {
|
||||
final List<Option> list = new ArrayList<>();
|
||||
list.add(Option.newDomainFile(DOMAIN_FILE_OPTION_NAME)
|
||||
.commandLineArgument(createArg("-associatedProgram"))
|
||||
.build());
|
||||
list.add(Option.newBoolean(STORE_REGS_AS_ATTRS_OPTION_NAME)
|
||||
.commandLineArgument("-storeRegs")
|
||||
.value(storeRegsAsAttrs)
|
||||
.build());
|
||||
list.add(Option.newBoolean(ADD_ALL_MEM_REFS_OPTION_NAME)
|
||||
.commandLineArgument("-allRefs")
|
||||
.value(addAllMemRefs)
|
||||
.build());
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Tenet++ Trace Format";
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoaderTier getTier() {
|
||||
return LoaderTier.GENERIC_TARGET_LOADER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTierPriority() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoadResults<? extends DomainObject> load(final ImporterSettings settings)
|
||||
throws IOException, CancelledException, VersionException, LoadException {
|
||||
Trace trace;
|
||||
String programPath = null;
|
||||
|
||||
for (final Option option : settings.options()) {
|
||||
final String name = option.getName();
|
||||
if (name.equals(DOMAIN_FILE_OPTION_NAME)) {
|
||||
programPath = (String) option.getValue();
|
||||
}
|
||||
else if (name.equals(STORE_REGS_AS_ATTRS_OPTION_NAME)) {
|
||||
storeRegsAsAttrs = (boolean) option.getValue();
|
||||
}
|
||||
else if (name.equals(ADD_ALL_MEM_REFS_OPTION_NAME)) {
|
||||
addAllMemRefs = (boolean) option.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
if ((programPath == null) || programPath.isBlank()) {
|
||||
throw new LoadException("No progam to associate with trace was given");
|
||||
}
|
||||
|
||||
Msg.info(this, "Loading trace for %s".formatted(programPath));
|
||||
|
||||
final DomainFile df = settings.project().getProjectData().getFile(programPath);
|
||||
if (!df.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) {
|
||||
throw new LoadException("Selected file is not a program");
|
||||
}
|
||||
Program program = null;
|
||||
|
||||
try {
|
||||
program = (Program) df.getDomainObject(this, false, false, settings.monitor());
|
||||
|
||||
final long start = System.currentTimeMillis();
|
||||
|
||||
trace = loadTrace(settings.provider(), settings.importName(), program,
|
||||
settings.consumer(), settings.log(), settings.monitor());
|
||||
|
||||
final long loadDone = System.currentTimeMillis();
|
||||
|
||||
// TODO: This is needed because
|
||||
// ghidra.trace.database.DBTraceContentHandler.createFile can't do
|
||||
// it, due to a lock in
|
||||
// ghidra.framework.data.GhidraFolderData.createFile
|
||||
final DBTraceObjectManager objManager = (DBTraceObjectManager) trace.getObjectManager();
|
||||
objManager.flushWbCaches();
|
||||
|
||||
final long flushDone = System.currentTimeMillis();
|
||||
|
||||
settings.log()
|
||||
.appendMsg("%d ms to load trace | %d ms to flush cache | %d ms total time"
|
||||
.formatted(loadDone - start, flushDone - loadDone, flushDone - start));
|
||||
|
||||
return new LoadResults<>(new Loaded<>(trace, settings));
|
||||
}
|
||||
finally {
|
||||
if (program != null) {
|
||||
program.release(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadInto(final Program program, final ImporterSettings settings)
|
||||
throws IOException, LoadException, CancelledException {
|
||||
throw new LoadException("Cannot add Trace to a program");
|
||||
}
|
||||
|
||||
private Trace loadTrace(final ByteProvider provider, final String name, final Program program,
|
||||
final Object consumer, final MessageLog log, final TaskMonitor monitor)
|
||||
throws LanguageNotFoundException, IOException, CancelledException {
|
||||
|
||||
this.program = program;
|
||||
final Language lang = program.getLanguage();
|
||||
defaultSpace = lang.getAddressFactory().getDefaultAddressSpace();
|
||||
|
||||
final Trace trace = new DBTrace(name, program.getCompilerSpec(), consumer);
|
||||
final TraceObjectManager om = trace.getObjectManager();
|
||||
|
||||
try (Transaction tx = trace.openTransaction("Import Tenet++ Trace: %s".formatted(name));
|
||||
BypassWriteCache bypass = om.withoutWriteCache()) {
|
||||
|
||||
om.createRootObject(TENET_PLUS_PLUS_SESSION_SCHEMA);
|
||||
|
||||
om.createObject(KeyPath.parse("Breakpoints"))
|
||||
.insert(Lifespan.ALL, ConflictResolution.DENY);
|
||||
|
||||
om.createObject(KeyPath.parse("Threads")).insert(Lifespan.ALL, ConflictResolution.DENY);
|
||||
|
||||
final Pattern ipPattern = Pattern.compile(
|
||||
"(?:^|,)%s=0x([0-9a-fA-F]+)".formatted(lang.getProgramCounter().getName()),
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
final BufferedReader lineCounter = new BufferedReader(
|
||||
new InputStreamReader(provider.getInputStream(0), StandardCharsets.UTF_8));
|
||||
final long numLines = lineCounter.lines().count();
|
||||
lineCounter.close();
|
||||
|
||||
final Map<String, TraceThread> tidToThreadMap = new HashMap<>();
|
||||
final List<ModuleEvent> modulesToLoad = new ArrayList<>();
|
||||
final List<ModuleEvent> modulesToUnload = new ArrayList<>();
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(provider.getInputStream(0), StandardCharsets.UTF_8))) {
|
||||
long curIp = 0;
|
||||
long lineNumber = 1;
|
||||
int errorCount = 0;
|
||||
long snapNumber = 0;
|
||||
|
||||
String line = reader.readLine();
|
||||
try {
|
||||
do {
|
||||
monitor.checkCancelled();
|
||||
|
||||
if (parseModuleEvents(log, line, modulesToLoad, modulesToUnload)) {
|
||||
// Module load/unload no need to continue parsing
|
||||
lineNumber++;
|
||||
line = reader.readLine();
|
||||
continue;
|
||||
}
|
||||
|
||||
final Matcher tidMatcher = TID_PATTERN.matcher(line);
|
||||
final Matcher ipMatcher = ipPattern.matcher(line);
|
||||
if (!tidMatcher.find()) {
|
||||
log.appendMsg(
|
||||
"Line %d: Unable to find TID, skipping...".formatted(lineNumber));
|
||||
errorCount++;
|
||||
}
|
||||
else if (!ipMatcher.find()) {
|
||||
log.appendMsg(
|
||||
"Line %d: Unable to find PC, skipping...".formatted(lineNumber));
|
||||
errorCount++;
|
||||
}
|
||||
else {
|
||||
final TraceSnapshot snapshot = trace.getTimeManager()
|
||||
.createSnapshot("Snapshot %d".formatted(snapNumber++));
|
||||
final Long snap = snapshot.getKey();
|
||||
final String tid = tidMatcher.group(1);
|
||||
if (!tidToThreadMap.containsKey(tid)) {
|
||||
final TraceThread thread = trace.getThreadManager()
|
||||
.createThread("Threads[%s]".formatted(tid),
|
||||
"Threads[%s]".formatted(tid), snap);
|
||||
tidToThreadMap.put(tid, thread);
|
||||
}
|
||||
final TraceThread traceThread = tidToThreadMap.get(tid);
|
||||
snapshot.setEventThread(traceThread);
|
||||
|
||||
if (snap == 0) {
|
||||
trace.getMemoryManager()
|
||||
.addRegion("Memory[ALL]", Lifespan.nowOn(0),
|
||||
rng(0x0, 0xFFFF_FFFF_FFFF_FFFFL), TraceMemoryFlag.READ,
|
||||
TraceMemoryFlag.EXECUTE, TraceMemoryFlag.WRITE);
|
||||
}
|
||||
|
||||
addModuleEventsToSnap(trace, snap, modulesToLoad, modulesToUnload);
|
||||
|
||||
curIp = Long.parseLong(ipMatcher.group(1), 16);
|
||||
|
||||
if (parseRegisterOperations(log, trace, traceThread, snap, curIp, line,
|
||||
lineNumber, numLines, monitor)) {
|
||||
parseMemoryOperations(trace, snap, curIp, line, lineNumber,
|
||||
numLines, monitor);
|
||||
}
|
||||
else {
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorCount >= ERROR_THRESHOLD) {
|
||||
throw new LoadException("Encountered too many errors with this trace");
|
||||
}
|
||||
|
||||
lineNumber++;
|
||||
|
||||
line = reader.readLine();
|
||||
}
|
||||
while (line != null);
|
||||
}
|
||||
catch (final CancelledException e) {
|
||||
throw e;
|
||||
}
|
||||
catch (final Exception e) {
|
||||
throw new LoadException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return trace;
|
||||
}
|
||||
|
||||
private void parseMemoryOperations(final Trace trace, final long snap, final long curIp,
|
||||
final String line, long lineNumber, long numLines, final TaskMonitor monitor)
|
||||
throws Exception {
|
||||
|
||||
final Matcher memMatcher = MEM_PATTERN.matcher(line);
|
||||
monitor.initialize(memMatcher.results().count(),
|
||||
"Parsing memory operations for line %d/%d".formatted(lineNumber, numLines));
|
||||
memMatcher.reset();
|
||||
// TODO Speed this up
|
||||
while (memMatcher.find()) {
|
||||
monitor.increment();
|
||||
|
||||
final RefType refType = switch (memMatcher.group(1)) {
|
||||
case "mr" -> RefType.READ;
|
||||
case "mw" -> RefType.WRITE;
|
||||
default -> RefType.READ_WRITE;
|
||||
};
|
||||
final Address address = toAddr(memMatcher.group(2));
|
||||
final byte[] bytes = NumericUtilities.convertStringToBytes(memMatcher.group(3));
|
||||
|
||||
// TODO: Should we do this?
|
||||
// If it's a read/write for the same address we're gonna assume it's
|
||||
// an aggregation of memory operations, so we can't say for certain
|
||||
// that the current instruction performed both operations
|
||||
if (addAllMemRefs || (refType != RefType.READ_WRITE)) {
|
||||
trace.getReferenceManager()
|
||||
.addMemoryReference(Lifespan.at(snap), addr(curIp),
|
||||
new AddressRangeImpl(address, bytes.length), refType,
|
||||
SourceType.IMPORTED, -1);
|
||||
}
|
||||
trace.getMemoryManager().putBytes(snap, address, ByteBuffer.wrap(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean parseModuleEvents(final MessageLog log, String line, List<ModuleEvent> load,
|
||||
List<ModuleEvent> unload) {
|
||||
final Matcher modLoadMatcher = MODULE_LOAD_PATTERN.matcher(line);
|
||||
if (modLoadMatcher.find()) {
|
||||
log.appendMsg("Found load of %s @ %s:%s".formatted(modLoadMatcher.group(3),
|
||||
modLoadMatcher.group(2), modLoadMatcher.group(1)));
|
||||
|
||||
final String mod = modLoadMatcher.group(3).replace("[", "").replace("]", "");
|
||||
|
||||
load.add(new ModuleEvent("Modules[%s]".formatted(mod), mod,
|
||||
rng(Long.parseLong(modLoadMatcher.group(1), 16),
|
||||
Long.parseLong(modLoadMatcher.group(2), 16))));
|
||||
return true;
|
||||
}
|
||||
|
||||
final Matcher modUnloadMatcher = MODULE_UNLOAD_PATTERN.matcher(line);
|
||||
if (modUnloadMatcher.find()) {
|
||||
log.appendMsg("Found unload of %s @ %s:%s".formatted(modUnloadMatcher.group(3),
|
||||
modUnloadMatcher.group(2), modUnloadMatcher.group(1)));
|
||||
|
||||
final String mod = modUnloadMatcher.group(3).replace("[", "").replace("]", "");
|
||||
|
||||
unload.add(new ModuleEvent("Modules[%s]".formatted(mod), mod,
|
||||
rng(Long.parseLong(modUnloadMatcher.group(1), 16),
|
||||
Long.parseLong(modUnloadMatcher.group(2), 16))));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean parseRegisterOperations(final MessageLog log, final Trace trace,
|
||||
final TraceThread traceThread, final long snap, final long curIp, final String line,
|
||||
final long lineNumber, long numLines, final TaskMonitor monitor) throws Exception {
|
||||
final TraceStackFrame frame =
|
||||
trace.getStackManager().getStack(traceThread, snap, true).getFrame(snap, 0, true);
|
||||
frame.setProgramCounter(Lifespan.nowOn(snap), addr(curIp));
|
||||
|
||||
final KeyPath traceRegistersPath = frame.getObject().getCanonicalPath().extend("Registers");
|
||||
final TraceObject traceRegisters =
|
||||
trace.getObjectManager().createObject(traceRegistersPath);
|
||||
traceRegisters.insert(Lifespan.ALL, ConflictResolution.DENY);
|
||||
|
||||
final Matcher regMatcher = REG_PATTERN.matcher(line);
|
||||
monitor.initialize(regMatcher.results().count(),
|
||||
"Parsing register operations for line %d/%d".formatted(lineNumber, numLines));
|
||||
regMatcher.reset();
|
||||
while (regMatcher.find()) {
|
||||
monitor.increment();
|
||||
|
||||
final BigInteger val = new BigInteger(regMatcher.group(2), 16);
|
||||
final Register regObj = trace.getProgramView().getRegister(regMatcher.group(1));
|
||||
if (regObj == null) {
|
||||
log.appendMsg("Line %d: Register %s not found in program language!"
|
||||
.formatted(lineNumber, regMatcher.group(1)));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (storeRegsAsAttrs) {
|
||||
traceRegisters.setElement(Lifespan.nowOn(snap), regObj.getName(), val.longValue());
|
||||
}
|
||||
trace.getMemoryManager()
|
||||
.getMemoryRegisterSpace(frame, true)
|
||||
.setValue(snap, new RegisterValue(regObj, val));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an address range in the processor's default space.
|
||||
*
|
||||
* @param min the minimum byte offset
|
||||
* @param max the maximum (inclusive) byte offset
|
||||
* @return the range
|
||||
*/
|
||||
private AddressRange rng(final long min, final long max) {
|
||||
return new AddressRangeImpl(addr(min), addr(max));
|
||||
}
|
||||
|
||||
private Address toAddr(final String addressString) {
|
||||
return AddressEvaluator.evaluate(program, addressString);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String validateOptions(final ByteProvider provider, final LoadSpec loadSpec,
|
||||
final List<Option> options, final Program program) {
|
||||
if (options != null) {
|
||||
for (final Option option : options) {
|
||||
final String name = option.getName();
|
||||
if (name.equals(DOMAIN_FILE_OPTION_NAME) &&
|
||||
!String.class.isAssignableFrom(option.getValueClass())) {
|
||||
return "Invalid type for option: " + name + " - " + option.getValueClass();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
/* ###
|
||||
* 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 static org.junit.Assert.*;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import generic.Unique;
|
||||
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest;
|
||||
import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin;
|
||||
import ghidra.app.plugin.core.debug.gui.model.DebuggerModelPlugin;
|
||||
import ghidra.app.plugin.core.debug.gui.register.DebuggerRegistersPlugin;
|
||||
import ghidra.app.plugin.core.debug.gui.thread.DebuggerThreadsPlugin;
|
||||
import ghidra.app.plugin.core.debug.gui.time.DebuggerTimePlugin;
|
||||
import ghidra.app.util.Option;
|
||||
import ghidra.app.util.bin.ByteArrayProvider;
|
||||
import ghidra.app.util.bin.ByteProvider;
|
||||
import ghidra.app.util.importer.MessageLog;
|
||||
import ghidra.app.util.opinion.Loader.ImporterSettings;
|
||||
import ghidra.framework.model.DomainObject;
|
||||
import ghidra.program.model.address.AddressSpace;
|
||||
import ghidra.trace.model.Trace;
|
||||
import ghidra.trace.model.memory.TraceMemorySpace;
|
||||
import ghidra.trace.model.thread.TraceThread;
|
||||
|
||||
public class TenetLoaderPlusPlusTest extends AbstractGhidraHeadedDebuggerTest {
|
||||
|
||||
/**
|
||||
* Create a byte array
|
||||
*
|
||||
* <p>
|
||||
* 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<Option> options =
|
||||
loader.getDefaultOptions(provider, loadSpec, null, false, false);
|
||||
final Option programOption = Unique.assertOne(
|
||||
options.stream().filter(o -> o.getName().equals(TenetLoader.DOMAIN_FILE_OPTION_NAME)));
|
||||
programOption.setValue(program.getDomainFile().getPathname());
|
||||
|
||||
final MessageLog log = new MessageLog();
|
||||
final ImporterSettings settings = new ImporterSettings(provider, "test", env.getProject(),
|
||||
"/", false, loadSpec, options, this, log, monitor);
|
||||
|
||||
final LoadResults<? extends DomainObject> results = loader.load(settings);
|
||||
if (!(results.getPrimary().domainObject instanceof final Trace trace)) {
|
||||
fail("Import result was not a trace");
|
||||
return null;
|
||||
}
|
||||
|
||||
return trace;
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Test that the loader continues on a bad register name
|
||||
*/
|
||||
@Test
|
||||
public void testBadRegisterNames() throws Exception {
|
||||
final String testFile = """
|
||||
tid=1,pc=0x1,rax=0x1234
|
||||
tid=1,pc=0x2,rax=0x1234
|
||||
tid=1,pc=0x3,rax=0x1234
|
||||
tid=1,pc=0x4
|
||||
tid=1,pc=0x5
|
||||
tid=1,pc=0x6,rax=0x1234
|
||||
tid=1,pc=0x7,rax=0x1234
|
||||
tid=1,pc=0x8,rax=0x1234
|
||||
tid=1,pc=0x9,rax=0x1234
|
||||
tid=1,pc=0xa,rax=0x1234
|
||||
""";
|
||||
final Trace trace = createTraceWithLoader(testFile);
|
||||
assertNotNull(trace);
|
||||
}
|
||||
|
||||
/*
|
||||
* Test the file content check for the loader
|
||||
*/
|
||||
@Test
|
||||
public void testFileContent() throws Exception {
|
||||
final String junk_line = "asdf\n";
|
||||
final String module_load_line = "Loaded image: 0x01:0x1337 -> MyImage\n";
|
||||
final String reg_line = "tid=1,pc=0x1,r0=0x1234\n";
|
||||
final String reg_line_no_tid = "pc=0x1,r0=0x1234\n";
|
||||
|
||||
this.createProgram();
|
||||
intoProject(program);
|
||||
|
||||
final TenetLoader loader = new TenetLoader();
|
||||
|
||||
ByteProvider provider = new ByteArrayProvider("test.tenet", (junk_line).getBytes("utf-8"));
|
||||
assertEquals(0, loader.findSupportedLoadSpecs(provider).size());
|
||||
|
||||
provider =
|
||||
new ByteArrayProvider("test.tenet", (module_load_line + junk_line).getBytes("utf-8"));
|
||||
assertEquals(0, loader.findSupportedLoadSpecs(provider).size());
|
||||
|
||||
provider = new ByteArrayProvider("test.tenet", (reg_line + junk_line).getBytes("utf-8"));
|
||||
assertEquals(0, loader.findSupportedLoadSpecs(provider).size());
|
||||
|
||||
provider = new ByteArrayProvider("test.tenet",
|
||||
(module_load_line + reg_line.repeat(TenetPlusPlusLoader.ERROR_THRESHOLD - 2) +
|
||||
junk_line).getBytes("utf-8"));
|
||||
assertEquals(0, loader.findSupportedLoadSpecs(provider).size());
|
||||
|
||||
provider = new ByteArrayProvider("test.tenet",
|
||||
reg_line_no_tid.repeat(TenetPlusPlusLoader.ERROR_THRESHOLD).getBytes("utf-8"));
|
||||
assertFalse(loader.findSupportedLoadSpecs(provider)
|
||||
.stream()
|
||||
.anyMatch(c -> c.getClass().equals(TenetPlusPlusLoader.class)));
|
||||
}
|
||||
|
||||
/*
|
||||
* Test the filename check for the loader
|
||||
*/
|
||||
@Test
|
||||
public void testFileName() throws Exception {
|
||||
final String testFile = """
|
||||
tid=1,pc=0x1,r0=0x1234
|
||||
tid=1,pc=0x2,r1=0x4321
|
||||
tid=1,pc=0x3,r1=0x4321
|
||||
tid=1,pc=0x4,r1=0x4321
|
||||
tid=1,pc=0x5,r1=0x4321
|
||||
tid=1,pc=0x6,r1=0x4321
|
||||
tid=1,pc=0x7,r1=0x4321
|
||||
tid=1,pc=0x8,r1=0x4321
|
||||
tid=1,pc=0x9,r1=0x4321
|
||||
tid=1,pc=0xa,r1=0x4321
|
||||
""";
|
||||
|
||||
this.createProgram();
|
||||
intoProject(program);
|
||||
|
||||
final TenetLoader loader = new TenetLoader();
|
||||
|
||||
ByteProvider provider = new ByteArrayProvider("test.tenet", testFile.getBytes("utf8"));
|
||||
Unique.assertOne(loader.findSupportedLoadSpecs(provider));
|
||||
|
||||
provider = new ByteArrayProvider("test.trace", testFile.getBytes("utf8"));
|
||||
Unique.assertOne(loader.findSupportedLoadSpecs(provider));
|
||||
|
||||
provider = new ByteArrayProvider("test.anythingelse", testFile.getBytes("utf8"));
|
||||
assertEquals(0, loader.findSupportedLoadSpecs(provider).size());
|
||||
}
|
||||
|
||||
/*
|
||||
* Automate loading a trace for manual testing purposes
|
||||
*/
|
||||
@Ignore
|
||||
public void testManual() throws Exception {
|
||||
final String testFile = """
|
||||
tid=1,pc=0x1,r0=0x1234
|
||||
tid=1,pc=0x2,r1=0x4321
|
||||
tid=1,pc=0x3,r1=0x4321
|
||||
tid=1,pc=0x4,r1=0x4321
|
||||
tid=1,pc=0x5,r1=0x4321
|
||||
tid=1,pc=0x6,r1=0x4321
|
||||
tid=1,pc=0x7,r1=0x4321
|
||||
tid=1,pc=0x8,r1=0x4321
|
||||
tid=1,pc=0x9,r1=0x4321
|
||||
tid=1,pc=0xa,r1=0x4321
|
||||
""";
|
||||
|
||||
addPlugin(tool, DebuggerRegistersPlugin.class);
|
||||
addPlugin(tool, DebuggerModelPlugin.class);
|
||||
addPlugin(tool, DebuggerThreadsPlugin.class);
|
||||
addPlugin(tool, DebuggerListingPlugin.class);
|
||||
addPlugin(tool, DebuggerTimePlugin.class);
|
||||
|
||||
final Trace trace = createTraceWithLoader(testFile);
|
||||
|
||||
traceManager.openTrace(trace);
|
||||
traceManager.activateTrace(trace);
|
||||
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
|
||||
/*
|
||||
* Test that the loader after more than {@link TenetLoader.ERROR_THRESHOLD}
|
||||
* lines with bad register names
|
||||
*/
|
||||
@Test
|
||||
public void testManyLinesBadRegisterNames() throws Exception {
|
||||
// Should fail after 10 lines with bad registers
|
||||
final String badTestFile =
|
||||
"tid=1,pc=0x1,rax=0x1234\n".repeat(TenetLoader.ERROR_THRESHOLD + 1);
|
||||
assertThrows(LoadException.class, () -> createTraceWithLoader(badTestFile));
|
||||
}
|
||||
|
||||
/*
|
||||
* Test that the loader fails after {@TenetLoader.ERROR_THRESHOLD} lines
|
||||
* without a PC
|
||||
*/
|
||||
@Test
|
||||
public void testManyLinesNoPc() throws Exception {
|
||||
// Should fail after {@TenetLoader.ERROR_THRESHOLD} lines with no PC
|
||||
final String badTestFile = "tid=1,r1=0x1234\n".repeat(TenetLoader.ERROR_THRESHOLD + 1);
|
||||
assertThrows(LoadException.class, () -> createTraceWithLoader(badTestFile));
|
||||
}
|
||||
|
||||
/*
|
||||
* Test how the loader handles overlapping memory reads/writes
|
||||
*/
|
||||
@Test
|
||||
public void testMemoryValueOverlap() throws Exception {
|
||||
final String testFile = """
|
||||
tid=1,pc=0x1,r0=0x1234,mr=0x1000:0000000000001337
|
||||
tid=2,pc=0x2,r1=0x4321,mw=0x2000:deadbeefdeadbeef
|
||||
tid=1,pc=0x3,mw=0x1000:87654321,mr=0x2000:c0ffee
|
||||
tid=1,pc=0x4,r1=0x4321
|
||||
tid=1,pc=0x5,r1=0x4321
|
||||
tid=1,pc=0x6,r1=0x4321
|
||||
tid=1,pc=0x7,r1=0x4321
|
||||
tid=1,pc=0x8,r1=0x4321
|
||||
tid=1,pc=0x9,r1=0x4321
|
||||
tid=1,pc=0xa,r1=0x4321
|
||||
""";
|
||||
|
||||
final Trace trace = createTraceWithLoader(testFile);
|
||||
|
||||
final AddressSpace addrSpace =
|
||||
trace.getBaseLanguage().getAddressFactory().getDefaultAddressSpace();
|
||||
final ByteBuffer buffer =
|
||||
ByteBuffer.allocate(trace.getBaseLanguage().getProgramCounter().getNumBytes());
|
||||
|
||||
trace.getMemoryManager().getBytes(0, addrSpace.getAddress(0x1000), buffer);
|
||||
assertArrayEquals(this.arr(0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x13, 0x37), buffer.array());
|
||||
|
||||
buffer.clear();
|
||||
trace.getMemoryManager().getBytes(1, addrSpace.getAddress(0x2000), buffer);
|
||||
assertArrayEquals(this.arr(0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef), buffer.array());
|
||||
|
||||
buffer.clear();
|
||||
trace.getMemoryManager().getBytes(2, addrSpace.getAddress(0x1000), buffer);
|
||||
assertArrayEquals(this.arr(0x87, 0x65, 0x43, 0x21, 0x00, 0x00, 0x13, 0x37), buffer.array());
|
||||
|
||||
buffer.clear();
|
||||
trace.getMemoryManager().getBytes(2, addrSpace.getAddress(0x2000), buffer);
|
||||
assertArrayEquals(this.arr(0xc0, 0xff, 0xee, 0xef, 0xde, 0xad, 0xbe, 0xef), buffer.array());
|
||||
}
|
||||
|
||||
/*
|
||||
* Test that the loader continues after a line with no PC
|
||||
*/
|
||||
@Test
|
||||
public void testNoPc() throws Exception {
|
||||
final String testFile = """
|
||||
tid=1,pc=0x1,r1=0x1234
|
||||
tid=1,r2=0x1234
|
||||
tid=1,pc=0x3,r1=0x4321
|
||||
tid=1,pc=0x4,r1=0x4321
|
||||
tid=1,pc=0x5,r1=0x4321
|
||||
tid=1,pc=0x6,r1=0x4321
|
||||
tid=1,pc=0x7,r1=0x4321
|
||||
tid=1,pc=0x8,r1=0x4321
|
||||
tid=1,pc=0x9,r1=0x4321
|
||||
tid=1,pc=0xa,r1=0x4321
|
||||
""";
|
||||
final Trace trace = createTraceWithLoader(testFile);
|
||||
assertNotNull(trace);
|
||||
}
|
||||
|
||||
/*
|
||||
* Test that the loader parses register values properly
|
||||
*/
|
||||
@Test
|
||||
public void testRegisterValues() throws Exception {
|
||||
final String testFile = """
|
||||
tid=1,pc=0x1,r0=0x1234
|
||||
tid=1,pc=0x2,r1=0x4321
|
||||
tid=1,pc=0x3,r1=0x4321
|
||||
tid=1,pc=0x4,r1=0x4321
|
||||
tid=1,pc=0x5,r1=0x4321
|
||||
tid=1,pc=0x6,r1=0x4321
|
||||
tid=1,pc=0x7,r1=0x4321
|
||||
tid=1,pc=0x8,r1=0x4321
|
||||
tid=1,pc=0x9,r1=0x4321
|
||||
tid=1,pc=0xa,r1=0x4321
|
||||
""";
|
||||
|
||||
final Trace trace = createTraceWithLoader(testFile);
|
||||
|
||||
final TraceThread thread = Unique.assertOne(trace.getThreadManager().getAllThreads());
|
||||
final TraceMemorySpace regs =
|
||||
trace.getMemoryManager().getMemoryRegisterSpace(thread, false);
|
||||
|
||||
assertNotNull(regs);
|
||||
assertEquals("1",
|
||||
regs.getValue(0, program.getLanguage().getProgramCounter())
|
||||
.getUnsignedValue()
|
||||
.toString(16));
|
||||
|
||||
assertEquals("2",
|
||||
regs.getValue(1, program.getLanguage().getProgramCounter())
|
||||
.getUnsignedValue()
|
||||
.toString(16));
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Test that the loader parses register values properly
|
||||
*/
|
||||
@Test
|
||||
public void testMultipleThreads() throws Exception {
|
||||
final String testFile = """
|
||||
tid=1,pc=0x1
|
||||
tid=2,pc=0x2
|
||||
tid=3,pc=0x3
|
||||
""";
|
||||
|
||||
final Trace trace = createTraceWithLoader(testFile);
|
||||
|
||||
assertEquals(3, trace.getThreadManager().getAllThreads().size());
|
||||
TraceThread thread =
|
||||
trace.getThreadManager().getThreadsByPath("Threads[1]").stream().findFirst().get();
|
||||
TraceMemorySpace t1_regs = trace.getMemoryManager().getMemoryRegisterSpace(thread, false);
|
||||
|
||||
assertNotNull(t1_regs);
|
||||
assertEquals("1",
|
||||
t1_regs.getValue(0, program.getLanguage().getProgramCounter())
|
||||
.getUnsignedValue()
|
||||
.toString(16));
|
||||
|
||||
thread = trace.getThreadManager().getThreadsByPath("Threads[2]").stream().findFirst().get();
|
||||
TraceMemorySpace t2_regs = trace.getMemoryManager().getMemoryRegisterSpace(thread, false);
|
||||
|
||||
assertNotNull(t2_regs);
|
||||
assertEquals("2",
|
||||
t2_regs.getValue(1, program.getLanguage().getProgramCounter())
|
||||
.getUnsignedValue()
|
||||
.toString(16));
|
||||
assertEquals("1",
|
||||
t1_regs.getValue(1, program.getLanguage().getProgramCounter())
|
||||
.getUnsignedValue()
|
||||
.toString(16));
|
||||
|
||||
thread = trace.getThreadManager().getThreadsByPath("Threads[3]").stream().findFirst().get();
|
||||
TraceMemorySpace t3_regs = trace.getMemoryManager().getMemoryRegisterSpace(thread, false);
|
||||
|
||||
assertNotNull(t3_regs);
|
||||
assertEquals("3",
|
||||
t3_regs.getValue(2, program.getLanguage().getProgramCounter())
|
||||
.getUnsignedValue()
|
||||
.toString(16));
|
||||
assertEquals("2",
|
||||
t2_regs.getValue(2, program.getLanguage().getProgramCounter())
|
||||
.getUnsignedValue()
|
||||
.toString(16));
|
||||
assertEquals("1",
|
||||
t1_regs.getValue(2, program.getLanguage().getProgramCounter())
|
||||
.getUnsignedValue()
|
||||
.toString(16));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
/* ###
|
||||
* 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.breakpoint.timeline;
|
||||
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.*;
|
||||
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JScrollPane;
|
||||
|
||||
import docking.*;
|
||||
import docking.action.DockingAction;
|
||||
import docking.action.MenuData;
|
||||
import docking.widgets.table.AnyObjectTableModel;
|
||||
import docking.widgets.table.GTable;
|
||||
import ghidra.app.plugin.core.byteviewer.ByteViewerActionContext;
|
||||
import ghidra.app.plugin.core.codebrowser.CodeViewerActionContext;
|
||||
import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingActionContext;
|
||||
import ghidra.app.plugin.core.debug.gui.register.DebuggerRegisterActionContext;
|
||||
import ghidra.app.services.DebuggerStaticMappingService;
|
||||
import ghidra.app.services.DebuggerTraceManagerService;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.program.model.address.*;
|
||||
import ghidra.program.util.ProgramLocation;
|
||||
import ghidra.trace.model.Lifespan;
|
||||
import ghidra.trace.model.Trace;
|
||||
import ghidra.trace.model.stack.TraceStackFrame;
|
||||
import ghidra.trace.model.symbol.TraceReference;
|
||||
import ghidra.trace.model.target.TraceObjectValue;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
public class BreakpointTimelineActions {
|
||||
protected record AddressSnap(Address address, Long snap) {}
|
||||
|
||||
private class BreakpointTimelineActionProvider extends ComponentProvider {
|
||||
private final JComponent component;
|
||||
private final List<AddressSnap> snaps;
|
||||
|
||||
public BreakpointTimelineActionProvider(Tool tool, String title,
|
||||
List<AddressSnap> snapList) {
|
||||
super(tool, title, "BreakpointTimelineActions");
|
||||
snaps = new ArrayList<>(snapList);
|
||||
|
||||
final AnyObjectTableModel<AddressSnap> model =
|
||||
new AnyObjectTableModel<>("AddressSnap", AddressSnap.class, "snap", "address");
|
||||
model.setModelData(snaps);
|
||||
final GTable table = new GTable(model);
|
||||
|
||||
table.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
if (e.getClickCount() == 2) {
|
||||
final int row = table.getSelectedRow();
|
||||
if (row != -1) {
|
||||
tool.getService(DebuggerTraceManagerService.class)
|
||||
.activateSnap(snaps.get(row).snap());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
component = new JScrollPane(table);
|
||||
setTransient();
|
||||
setVisible(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JComponent getComponent() {
|
||||
return component;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private enum BreakType {
|
||||
EXECUTE("Execution"), READ("Memory Read"), WRITE("Memory Write"), ACCESS("Memory Access");
|
||||
|
||||
String menuName;
|
||||
|
||||
BreakType(String menuName) {
|
||||
this.menuName = menuName;
|
||||
}
|
||||
|
||||
String plural() {
|
||||
return switch (this) {
|
||||
case EXECUTE -> "executions";
|
||||
case READ -> "memory reads";
|
||||
case WRITE -> "memory writes";
|
||||
case ACCESS -> "memory accesses";
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private enum SearchType {
|
||||
FIRST("First..."),
|
||||
PREVIOUS("Previous..."),
|
||||
NEXT("Next..."),
|
||||
FINAL("Final..."),
|
||||
ALL("Show All...");
|
||||
|
||||
String menuGroup;
|
||||
|
||||
SearchType(String menuGroup) {
|
||||
this.menuGroup = menuGroup;
|
||||
}
|
||||
}
|
||||
|
||||
private class TimelineAction extends DockingAction {
|
||||
private final BreakType breakType;
|
||||
private final SearchType searchType;
|
||||
private final PluginTool tool;
|
||||
|
||||
private TimelineAction(PluginTool tool, SearchType searchType, BreakType breakType) {
|
||||
super(breakType.menuName, "BreakpointTimelineHitAction");
|
||||
this.breakType = breakType;
|
||||
this.tool = tool;
|
||||
this.searchType = searchType;
|
||||
|
||||
if (searchType != SearchType.ALL) {
|
||||
setPopupMenuData(new MenuData(
|
||||
new String[] { "Go to...", searchType.menuGroup, breakType.menuName }, null,
|
||||
"BreakpointTimelineActions"));
|
||||
}
|
||||
else {
|
||||
setPopupMenuData(
|
||||
new MenuData(new String[] { searchType.menuGroup, breakType.menuName }, null,
|
||||
"BreakpointTimelineActions"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionContext context) {
|
||||
final AddressSet addrRange = getAddessRangeFromContext(context);
|
||||
final Lifespan lifespan = getLifespan();
|
||||
final Trace currentTrace =
|
||||
tool.getService(DebuggerTraceManagerService.class).getCurrentTrace();
|
||||
|
||||
switch (breakType) {
|
||||
case EXECUTE -> handleExecutionAction(addrRange, lifespan, currentTrace);
|
||||
case READ, WRITE, ACCESS -> handleMemoryAction(addrRange, lifespan, currentTrace);
|
||||
}
|
||||
}
|
||||
|
||||
private AddressSet getAddessRangeFromContext(ActionContext context) {
|
||||
return switch (context) {
|
||||
case final CodeViewerActionContext c -> getAddressRangeFromCodeViewerActionContext(
|
||||
c);
|
||||
case final DebuggerListingActionContext c -> getAddressRangeFromDebuggerListingActionContext(
|
||||
c);
|
||||
case final DebuggerRegisterActionContext c -> getAddressRangeFromDebuggerRegisterActionContext(
|
||||
c);
|
||||
case final ByteViewerActionContext c -> getAddressRangeFromByteViewerActionContext(
|
||||
c);
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
private AddressSet getAddressRangeFromByteViewerActionContext(ByteViewerActionContext c) {
|
||||
final AddressSet dynamicSet = new AddressSet();
|
||||
|
||||
for (final AddressRange range : c.getSelection().getAddressRanges()) {
|
||||
dynamicSet.add(range.getMinAddress(), range.getMaxAddress());
|
||||
}
|
||||
return dynamicSet;
|
||||
}
|
||||
|
||||
private AddressSet getAddressRangeFromCodeViewerActionContext(CodeViewerActionContext c) {
|
||||
final Trace trace =
|
||||
tool.getService(DebuggerTraceManagerService.class).getCurrentTrace();
|
||||
final DebuggerStaticMappingService staticMappingService =
|
||||
tool.getService(DebuggerStaticMappingService.class);
|
||||
final AddressSet dynamicSet = new AddressSet();
|
||||
|
||||
if (c.getSelection().isEmpty()) {
|
||||
final ProgramLocation dynamicLocation =
|
||||
staticMappingService.getDynamicLocationFromStatic(trace.getProgramView(),
|
||||
new ProgramLocation(c.getProgram(), c.getAddress()));
|
||||
if (dynamicLocation == null) {
|
||||
Msg.showInfo(null, null, "No mapping", "No mapping for %s @ 0x%x"
|
||||
.formatted(c.getProgram(), c.getAddress().getOffset()));
|
||||
}
|
||||
else {
|
||||
dynamicSet.add(dynamicLocation.getAddress());
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (final AddressRange range : c.getSelection().getAddressRanges()) {
|
||||
final ProgramLocation startDynamicLocation =
|
||||
staticMappingService.getDynamicLocationFromStatic(trace.getProgramView(),
|
||||
new ProgramLocation(c.getProgram(), range.getMinAddress()));
|
||||
if (startDynamicLocation == null) {
|
||||
Msg.showInfo(null, null, "No mapping", "No mapping for %s @ 0x%x"
|
||||
.formatted(c.getProgram(), range.getMinAddress().getOffset()));
|
||||
continue;
|
||||
}
|
||||
final ProgramLocation endDynamicLocation =
|
||||
staticMappingService.getDynamicLocationFromStatic(trace.getProgramView(),
|
||||
new ProgramLocation(c.getProgram(), range.getMaxAddress()));
|
||||
if (endDynamicLocation == null) {
|
||||
Msg.showInfo(null, null, "No mapping", "No mapping for %s @ 0x%x"
|
||||
.formatted(c.getProgram(), range.getMaxAddress().getOffset()));
|
||||
continue;
|
||||
}
|
||||
dynamicSet.add(startDynamicLocation.getAddress(),
|
||||
endDynamicLocation.getAddress());
|
||||
}
|
||||
}
|
||||
return dynamicSet;
|
||||
}
|
||||
|
||||
private AddressSet getAddressRangeFromDebuggerListingActionContext(
|
||||
DebuggerListingActionContext c) {
|
||||
final AddressSet dynamicSet = new AddressSet();
|
||||
if (c.getSelection().isEmpty()) {
|
||||
dynamicSet.add(c.getAddress());
|
||||
}
|
||||
else {
|
||||
for (final AddressRange range : c.getSelection().getAddressRanges()) {
|
||||
dynamicSet.add(range.getMinAddress(), range.getMaxAddress());
|
||||
}
|
||||
}
|
||||
return dynamicSet;
|
||||
}
|
||||
|
||||
private AddressSet getAddressRangeFromDebuggerRegisterActionContext(
|
||||
DebuggerRegisterActionContext c) {
|
||||
final AddressSpace addressSpace = tool.getService(DebuggerTraceManagerService.class)
|
||||
.getCurrentTrace()
|
||||
.getProgramView()
|
||||
.getAddressFactory()
|
||||
.getDefaultAddressSpace();
|
||||
final Long regValue = c.getSelected().getValue().longValue();
|
||||
final Address addr = addressSpace.getAddress(regValue);
|
||||
final AddressSet addressSet = new AddressSet();
|
||||
addressSet.add(addr);
|
||||
return addressSet;
|
||||
}
|
||||
|
||||
private Lifespan getLifespan() {
|
||||
final long currentSnap =
|
||||
tool.getService(DebuggerTraceManagerService.class).getCurrentSnap();
|
||||
return switch (searchType) {
|
||||
case ALL, FINAL, FIRST -> Lifespan.ALL;
|
||||
case NEXT -> Lifespan.nowOn(currentSnap + 1);
|
||||
case PREVIOUS -> Lifespan.before(currentSnap);
|
||||
};
|
||||
}
|
||||
|
||||
private void handleExecutionAction(AddressSet addrSet, Lifespan lifespan,
|
||||
Trace currentTrace) {
|
||||
final List<? extends TraceObjectValue> hits = currentTrace.getObjectManager()
|
||||
.getRootSchema()
|
||||
.getContext()
|
||||
.getAllSchemas()
|
||||
.stream()
|
||||
.filter(e -> e.getInterfaces().contains(TraceStackFrame.class))
|
||||
.map(s -> s.checkAliasedAttribute(TraceStackFrame.KEY_PC))
|
||||
.flatMap(e -> addrSet.stream()
|
||||
.flatMap(addrRange -> currentTrace.getObjectManager()
|
||||
.getValuesIntersecting(lifespan, addrRange, e)
|
||||
.stream()))
|
||||
.sorted(Comparator.comparingLong(TraceObjectValue::getMinSnap))
|
||||
.toList();
|
||||
|
||||
if (hits.size() > 0) {
|
||||
if (searchType == SearchType.ALL) {
|
||||
new BreakpointTimelineActionProvider(tool,
|
||||
"All executions in %s - %s".formatted(addrSet.getMinAddress(),
|
||||
addrSet.getMaxAddress()),
|
||||
hits.stream()
|
||||
.map(c -> new AddressSnap(c.castValue(), c.getMinSnap()))
|
||||
.toList());
|
||||
return;
|
||||
}
|
||||
final Long snap = switch (searchType) {
|
||||
case FINAL, PREVIOUS -> hits.getLast().getMinSnap();
|
||||
case FIRST, NEXT -> hits.getFirst().getMinSnap();
|
||||
default -> null;
|
||||
};
|
||||
if (snap == null) {
|
||||
return;
|
||||
}
|
||||
tool.getService(DebuggerTraceManagerService.class).activateSnap(snap);
|
||||
}
|
||||
else if (searchType == SearchType.ALL) {
|
||||
Msg.showInfo(this, null, "%s search".formatted(breakType.name()),
|
||||
"There are no executions at this location".formatted(breakType.name()));
|
||||
}
|
||||
else {
|
||||
Msg.showInfo(this, null, "%s search".formatted(breakType.name()),
|
||||
"There is no %s execution at this location"
|
||||
.formatted(searchType.name().toLowerCase()));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMemoryAction(AddressSet addrSet, Lifespan lifespan, Trace currentTrace) {
|
||||
final List<? extends TraceReference> hits = addrSet.stream()
|
||||
.flatMap(addrRange -> currentTrace.getReferenceManager()
|
||||
.getReferencesToRange(lifespan, addrRange)
|
||||
.stream())
|
||||
.sorted(Comparator.comparingLong(TraceReference::getStartSnap))
|
||||
.toList();
|
||||
|
||||
if (hits.size() > 0) {
|
||||
if (searchType == SearchType.ALL) {
|
||||
new BreakpointTimelineActionProvider(tool,
|
||||
"All %s in %s - %s".formatted(breakType.name(), addrSet.getMinAddress(),
|
||||
addrSet.getMaxAddress()),
|
||||
hits.stream()
|
||||
.map(c -> new AddressSnap(c.getFromAddress(), c.getStartSnap()))
|
||||
.toList());
|
||||
return;
|
||||
}
|
||||
final Long snap = switch (searchType) {
|
||||
case FINAL, PREVIOUS -> hits.getLast().getStartSnap();
|
||||
case FIRST, NEXT -> hits.getFirst().getStartSnap();
|
||||
default -> null;
|
||||
};
|
||||
if (snap == null) {
|
||||
return;
|
||||
}
|
||||
tool.getService(DebuggerTraceManagerService.class).activateSnap(snap);
|
||||
}
|
||||
else if (searchType == SearchType.ALL) {
|
||||
Msg.showInfo(this, null, "%s search".formatted(breakType.name()),
|
||||
"There are no %s to this location".formatted(breakType.plural()));
|
||||
}
|
||||
else {
|
||||
Msg.showInfo(this, null, "%s search".formatted(breakType.name()),
|
||||
"There is no %s %s to this location".formatted(searchType.name().toLowerCase(),
|
||||
breakType.menuName.toLowerCase()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabledForContext(ActionContext context) {
|
||||
return ((context instanceof CodeViewerActionContext) ||
|
||||
(context instanceof DebuggerListingActionContext) ||
|
||||
(context instanceof DebuggerRegisterActionContext) ||
|
||||
(context instanceof ByteViewerActionContext));
|
||||
}
|
||||
}
|
||||
|
||||
private final PluginTool tool;
|
||||
List<TimelineAction> actions;
|
||||
|
||||
BreakpointTimelineActions(PluginTool tool) {
|
||||
this.tool = tool;
|
||||
actions = new ArrayList<>();
|
||||
for (final SearchType searchType : SearchType.values()) {
|
||||
for (final BreakType breakType : BreakType.values()) {
|
||||
final TimelineAction action = new TimelineAction(tool, searchType, breakType);
|
||||
tool.addAction(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
for (final TimelineAction action : actions) {
|
||||
tool.removeAction(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,28 +180,7 @@ class BreakpointTimelinePanel extends JPanel {
|
||||
}
|
||||
|
||||
if (!BreakpointTimelinePanel.singleColumn) {
|
||||
final long numCells = visibleEnd - visibleStart;
|
||||
|
||||
double xSideLength;
|
||||
double ySideLength;
|
||||
|
||||
final double xPixelsPerCell = Math.ceil(Math.sqrt((numCells * width) / height));
|
||||
if ((Math.floor((xPixelsPerCell * height) / width) * xPixelsPerCell) < numCells) {
|
||||
xSideLength = (height / Math.ceil((height * xPixelsPerCell) / width));
|
||||
}
|
||||
else {
|
||||
xSideLength = width / xPixelsPerCell;
|
||||
}
|
||||
|
||||
final double yPixelsPerCell = Math.ceil(Math.sqrt((numCells * height) / width));
|
||||
if ((Math.floor((yPixelsPerCell * width) / height) * yPixelsPerCell) < numCells) {
|
||||
ySideLength = (width / Math.ceil((width * yPixelsPerCell) / height));
|
||||
}
|
||||
else {
|
||||
ySideLength = height / yPixelsPerCell;
|
||||
}
|
||||
|
||||
final long potentialSideLength = (long) Math.max(xSideLength, ySideLength);
|
||||
final long potentialSideLength = getPotentialSideLength(width, height);
|
||||
|
||||
cellWidth = Math.max(defaultCellSize, potentialSideLength);
|
||||
cellHeight = cellWidth;
|
||||
@@ -250,6 +229,34 @@ class BreakpointTimelinePanel extends JPanel {
|
||||
repaint();
|
||||
}
|
||||
|
||||
private long getPotentialSideLength(int width, int height) {
|
||||
final long numCells = visibleEnd - visibleStart;
|
||||
|
||||
double xSideLength;
|
||||
double ySideLength;
|
||||
|
||||
double xPixelsPerCell = Math.ceil(Math.sqrt((numCells * width) / height));
|
||||
if (xPixelsPerCell == 0) {
|
||||
xPixelsPerCell = width;
|
||||
}
|
||||
if ((Math.floor((xPixelsPerCell * height) / width) * xPixelsPerCell) < numCells) {
|
||||
xSideLength = (height / Math.ceil((height * xPixelsPerCell) / width));
|
||||
}
|
||||
else {
|
||||
xSideLength = width / xPixelsPerCell;
|
||||
}
|
||||
|
||||
final double yPixelsPerCell = Math.ceil(Math.sqrt((numCells * height) / width));
|
||||
if ((Math.floor((yPixelsPerCell * width) / height) * yPixelsPerCell) < numCells) {
|
||||
ySideLength = (width / Math.ceil((width * yPixelsPerCell) / height));
|
||||
}
|
||||
else {
|
||||
ySideLength = height / yPixelsPerCell;
|
||||
}
|
||||
|
||||
return (long) Math.max(xSideLength, ySideLength);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToolTipText(MouseEvent event) {
|
||||
final Optional<BreakpointTimelinePanel.CachedIndex> first =
|
||||
|
||||
@@ -22,6 +22,7 @@ import ghidra.app.plugin.core.debug.AbstractDebuggerPlugin;
|
||||
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
|
||||
import ghidra.app.plugin.core.debug.event.TraceActivatedPluginEvent;
|
||||
import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent;
|
||||
import ghidra.app.services.DebuggerStaticMappingService;
|
||||
import ghidra.app.services.DebuggerTraceManagerService;
|
||||
import ghidra.framework.plugintool.*;
|
||||
import ghidra.framework.plugintool.util.PluginStatus;
|
||||
@@ -33,11 +34,13 @@ import ghidra.trace.model.Trace;
|
||||
category = PluginCategoryNames.DEBUGGER,
|
||||
packageName = DebuggerPluginPackage.NAME,
|
||||
status = PluginStatus.UNSTABLE,
|
||||
servicesRequired = { DebuggerTraceManagerService.class, },
|
||||
servicesRequired = { DebuggerTraceManagerService.class, DebuggerStaticMappingService.class },
|
||||
eventsConsumed = { TraceClosedPluginEvent.class, TraceActivatedPluginEvent.class, }
|
||||
)
|
||||
public class BreakpointTimelinePlugin extends AbstractDebuggerPlugin {
|
||||
BreakpointTimelineProvider provider;
|
||||
BreakpointTimelineActions actions;
|
||||
|
||||
private Trace currentTrace;
|
||||
|
||||
private final Map<Trace, List<BreakpointTimelineProvider>> traceSpecificZoomProviders =
|
||||
@@ -54,6 +57,7 @@ public class BreakpointTimelinePlugin extends AbstractDebuggerPlugin {
|
||||
|
||||
@Override
|
||||
protected void dispose() {
|
||||
actions.dispose();
|
||||
for (final var providers : traceSpecificZoomProviders.values()) {
|
||||
for (final var provider : providers) {
|
||||
tool.removeComponentProvider(provider);
|
||||
@@ -76,6 +80,7 @@ public class BreakpointTimelinePlugin extends AbstractDebuggerPlugin {
|
||||
protected void init() {
|
||||
super.init();
|
||||
provider = new BreakpointTimelineProvider(this);
|
||||
actions = new BreakpointTimelineActions(getTool());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -26,6 +26,8 @@ import docking.ComponentProvider;
|
||||
import docking.action.DockingAction;
|
||||
import docking.action.ToolBarData;
|
||||
import generic.theme.GIcon;
|
||||
import ghidra.framework.model.DomainObjectEvent;
|
||||
import ghidra.lifecycle.Internal;
|
||||
import ghidra.program.model.address.AddressRange;
|
||||
import ghidra.program.model.symbol.RefType;
|
||||
import ghidra.trace.model.*;
|
||||
@@ -36,7 +38,7 @@ import ghidra.trace.model.symbol.TraceReference;
|
||||
import ghidra.trace.model.target.TraceObjectValue;
|
||||
import ghidra.trace.util.TraceEvents;
|
||||
|
||||
class BreakpointTimelineProvider extends ComponentProvider {
|
||||
public class BreakpointTimelineProvider extends ComponentProvider {
|
||||
record BreakpointHitEvent(long snap, TraceBreakpointKind breakType, String breakpointName) {}
|
||||
|
||||
private class BreakpointTimeOverviewEventListener extends TraceDomainObjectListener {
|
||||
@@ -44,6 +46,10 @@ class BreakpointTimelineProvider extends ComponentProvider {
|
||||
public BreakpointTimeOverviewEventListener() {
|
||||
listenFor(TraceEvents.BREAKPOINT_CHANGED, this::breakpointChanged);
|
||||
listenFor(TraceEvents.BREAKPOINT_DELETED, this::breakpointDeleted);
|
||||
listenFor(TraceEvents.SNAPSHOT_ADDED, this::snapshotEvent);
|
||||
listenFor(TraceEvents.SNAPSHOT_DELETED, this::snapshotEvent);
|
||||
listenFor(TraceEvents.SNAPSHOT_CHANGED, this::snapshotEvent);
|
||||
listenForUntyped(DomainObjectEvent.RESTORED, e -> snapshotEvent());
|
||||
}
|
||||
|
||||
void breakpointChanged(TraceBreakpointLocation tb) {
|
||||
@@ -56,10 +62,21 @@ class BreakpointTimelineProvider extends ComponentProvider {
|
||||
breakpointTimelinePlugin.refreshAllProviders(null);
|
||||
}
|
||||
|
||||
void snapshotEvent() {
|
||||
final long newMaxSnap =
|
||||
Objects.requireNonNullElse(currentTrace.getTimeManager().getMaxSnap(), 0)
|
||||
.longValue();
|
||||
|
||||
if (newMaxSnap != curMaxSnap) {
|
||||
breakpointTimelinePanel.setEventsAndVisibleRange(breakpointHits, 0, newMaxSnap);
|
||||
curMaxSnap = newMaxSnap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class CloseAllZoomWindowsAction extends DockingAction {
|
||||
private final GIcon ICON = new GIcon("icon.debugger.breakpoint.timeline.close_all_zoom_windows");
|
||||
private final GIcon ICON =
|
||||
new GIcon("icon.debugger.breakpoint.timeline.close_all_zoom_windows");
|
||||
|
||||
CloseAllZoomWindowsAction(ComponentProvider provider) {
|
||||
super("Close all zoom windows", provider.getOwner());
|
||||
@@ -90,7 +107,8 @@ class BreakpointTimelineProvider extends ComponentProvider {
|
||||
|
||||
private class ToggleGridAction extends DockingAction {
|
||||
private final GIcon OUTLINE_ICON = new GIcon("icon.debugger.breakpoint.timeline.outline");
|
||||
private final GIcon NO_OUTLINE_ICON = new GIcon("icon.debugger.breakpoint.timeline.no_outline");
|
||||
private final GIcon NO_OUTLINE_ICON =
|
||||
new GIcon("icon.debugger.breakpoint.timeline.no_outline");
|
||||
private boolean grid = true;
|
||||
|
||||
ToggleGridAction(ComponentProvider provider) {
|
||||
@@ -160,13 +178,30 @@ class BreakpointTimelineProvider extends ComponentProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private Trace currentTrace;
|
||||
private final BreakpointTimelinePanel breakpointTimelinePanel;
|
||||
@Internal
|
||||
public static Iterator<? extends TraceObjectValue> getTraceObjectValuesWithPCsIntersectingRange(
|
||||
Trace trace, AddressRange range) {
|
||||
return trace.getObjectManager()
|
||||
.getRootSchema()
|
||||
.getContext()
|
||||
.getAllSchemas()
|
||||
.stream()
|
||||
.filter(s -> s.getInterfaces().contains(TraceStackFrame.class))
|
||||
.map(s -> s.checkAliasedAttribute(TraceStackFrame.KEY_PC))
|
||||
.flatMap(e -> trace.getObjectManager()
|
||||
.getValuesIntersecting(Lifespan.ALL, range, e)
|
||||
.stream())
|
||||
.sorted(Comparator.comparingLong(TraceObjectValue::getMinSnap))
|
||||
.iterator();
|
||||
}
|
||||
|
||||
private final BreakpointTimelinePanel breakpointTimelinePanel;
|
||||
private final JPanel wrapperPanel;
|
||||
private final BreakpointTimelinePlugin breakpointTimelinePlugin;
|
||||
private final BreakpointTimeOverviewEventListener listener =
|
||||
new BreakpointTimeOverviewEventListener();
|
||||
private Trace currentTrace;
|
||||
private long curMaxSnap;
|
||||
private List<BreakpointHitEvent> breakpointHits;
|
||||
|
||||
BreakpointTimelineProvider(BreakpointTimelinePlugin breakpointTimelinePlugin) {
|
||||
@@ -217,7 +252,8 @@ class BreakpointTimelineProvider extends ComponentProvider {
|
||||
|
||||
private void findAndAddAllBreakpointHitsAtLocation(TraceBreakpointLocation breakpointLocation,
|
||||
AddressRange range, String kind) {
|
||||
for (final TraceBreakpointKind breakpointKind : TraceBreakpointKindSet.decode(kind, false)) {
|
||||
for (final TraceBreakpointKind breakpointKind : TraceBreakpointKindSet.decode(kind,
|
||||
false)) {
|
||||
switch (breakpointKind) {
|
||||
case HW_EXECUTE, SW_EXECUTE -> findAndAddExecuteBreakpointHits(breakpointLocation,
|
||||
range);
|
||||
@@ -229,9 +265,11 @@ class BreakpointTimelineProvider extends ComponentProvider {
|
||||
|
||||
private void findAndAddExecuteBreakpointHits(TraceBreakpointLocation breakpointLocation,
|
||||
AddressRange range) {
|
||||
final Collection<? extends TraceObjectValue> intersecting = currentTrace.getObjectManager()
|
||||
.getValuesIntersecting(Lifespan.ALL, range, TraceStackFrame.KEY_PC);
|
||||
for (final TraceObjectValue tov : intersecting) {
|
||||
final Iterator<? extends TraceObjectValue> intersecting =
|
||||
getTraceObjectValuesWithPCsIntersectingRange(currentTrace, range);
|
||||
|
||||
while (intersecting.hasNext()) {
|
||||
final TraceObjectValue tov = intersecting.next();
|
||||
breakpointHits.add(new BreakpointHitEvent(tov.getMinSnap(),
|
||||
TraceBreakpointKind.SW_EXECUTE, breakpointLocation.getName(tov.getMinSnap())));
|
||||
}
|
||||
@@ -302,15 +340,17 @@ class BreakpointTimelineProvider extends ComponentProvider {
|
||||
currentTrace.removeListener(listener);
|
||||
}
|
||||
currentTrace = trace;
|
||||
curMaxSnap = 0;
|
||||
refreshBreakpointHits();
|
||||
|
||||
if (currentTrace != null) {
|
||||
breakpointTimelinePanel.setEventsAndVisibleRange(breakpointHits, 0,
|
||||
currentTrace.getTimeManager().getMaxSnap());
|
||||
curMaxSnap = Objects.requireNonNullElse(currentTrace.getTimeManager().getMaxSnap(), 0)
|
||||
.longValue();
|
||||
breakpointTimelinePanel.setEventsAndVisibleRange(breakpointHits, 0, curMaxSnap);
|
||||
currentTrace.addListener(listener);
|
||||
}
|
||||
else {
|
||||
breakpointTimelinePanel.setEventsAndVisibleRange(null, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,14 @@ package ghidra.app.plugin.core.debug.gui.listing;
|
||||
|
||||
import static ghidra.app.plugin.core.debug.gui.DebuggerResources.GROUP_TRANSIENT_VIEWS;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import docking.ComponentProvider;
|
||||
import ghidra.debug.api.listing.DebuggerListing;
|
||||
import org.jdom2.Element;
|
||||
|
||||
import docking.ActionContext;
|
||||
@@ -39,6 +43,7 @@ import ghidra.app.util.viewer.format.FormatManager;
|
||||
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.debug.api.tracemgr.DebuggerCoordinates;
|
||||
import ghidra.framework.options.SaveState;
|
||||
@@ -180,6 +185,22 @@ public class DebuggerListingPlugin extends AbstractCodeBrowserPlugin<DebuggerLis
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DebuggerListing createNewListing() {
|
||||
return createNewDisconnectedProvider();
|
||||
}
|
||||
|
||||
public String findNextCustomTitle() {
|
||||
List<String> allTitles = disconnectedProviders.stream().map(ComponentProvider::getTitle).toList();
|
||||
int i;
|
||||
for (i = 0; i < allTitles.size(); i++) {
|
||||
if (!allTitles.contains("Dynamic %d".formatted(i))) {
|
||||
return "Dynamic %d".formatted(i);
|
||||
}
|
||||
}
|
||||
return "Dynamic %d".formatted(i);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setView(AddressSetView addrSet) {
|
||||
TraceProgramView view = current.getView();
|
||||
@@ -352,6 +373,14 @@ public class DebuggerListingPlugin extends AbstractCodeBrowserPlugin<DebuggerLis
|
||||
return goTo(loc, centerOnScreen);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DebuggerListing> getAllListings() {
|
||||
final List<DebuggerListing> ret = new ArrayList<>();
|
||||
ret.add(connectedProvider);
|
||||
ret.addAll(disconnectedProviders);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeDataState(SaveState saveState) {
|
||||
SaveState connectedProviderState = new SaveState();
|
||||
|
||||
@@ -67,6 +67,7 @@ import ghidra.async.AsyncDebouncer;
|
||||
import ghidra.async.AsyncTimer;
|
||||
import ghidra.debug.api.action.*;
|
||||
import ghidra.debug.api.control.ControlMode;
|
||||
import ghidra.debug.api.listing.DebuggerListing;
|
||||
import ghidra.debug.api.listing.MultiBlendedListingBackgroundColorModel;
|
||||
import ghidra.debug.api.modules.DebuggerStaticMappingChangeListener;
|
||||
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
|
||||
@@ -90,7 +91,7 @@ import ghidra.util.datastruct.ListenerSet;
|
||||
import utilities.util.SuppressableCallback;
|
||||
import utilities.util.SuppressableCallback.Suppression;
|
||||
|
||||
public class DebuggerListingProvider extends CodeViewerProvider {
|
||||
public class DebuggerListingProvider extends CodeViewerProvider implements DebuggerListing {
|
||||
|
||||
private static final AutoConfigState.ClassHandler<
|
||||
DebuggerListingProvider> CONFIG_STATE_HANDLER =
|
||||
@@ -500,6 +501,9 @@ public class DebuggerListingProvider extends CodeViewerProvider {
|
||||
CONFIG_STATE_HANDLER.writeConfigState(this, saveState);
|
||||
trackingTrait.writeConfigState(saveState);
|
||||
readsMemTrait.writeConfigState(saveState);
|
||||
if (!isMainListing() && getTitle() != null) {
|
||||
saveState.putString("title", getTitle());
|
||||
}
|
||||
}
|
||||
|
||||
void readConfigState(SaveState saveState) {
|
||||
@@ -514,7 +518,9 @@ public class DebuggerListingProvider extends CodeViewerProvider {
|
||||
CONFIG_STATE_HANDLER.readConfigState(this, saveState);
|
||||
trackingTrait.readConfigState(saveState);
|
||||
readsMemTrait.readConfigState(saveState);
|
||||
|
||||
if (!isMainListing() && saveState.hasValue("title")) {
|
||||
setTitle(saveState.getString("title", ""));
|
||||
}
|
||||
if (isMainListing()) {
|
||||
followsCurrentThread = true;
|
||||
}
|
||||
|
||||
@@ -884,7 +884,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter
|
||||
if (sel == null || sel.size() != 1) {
|
||||
return;
|
||||
}
|
||||
mapModuleTo(sel.iterator().next());
|
||||
mapModuleTo(sel.iterator().next(), current.getSnap());
|
||||
}
|
||||
|
||||
private void activatedMapSections(ActionContext context) {
|
||||
@@ -940,7 +940,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter
|
||||
}
|
||||
|
||||
private void activatedMapMissingModule(DebuggerMissingModuleActionContext context) {
|
||||
mapModuleTo(context.getModule());
|
||||
mapModuleTo(context.getModule(), context.getSnap());
|
||||
}
|
||||
|
||||
private void activatedMapMissingProgramRetry(DebuggerMissingProgramActionContext context) {
|
||||
@@ -1149,7 +1149,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter
|
||||
promptModuleProposal(proposal, NO_MODULES_PROPOSAL_SEL);
|
||||
}
|
||||
|
||||
protected void mapModuleTo(TraceModule module) {
|
||||
protected void mapModuleTo(TraceModule module, long snap) {
|
||||
if (staticMappingService == null) {
|
||||
return;
|
||||
}
|
||||
@@ -1157,8 +1157,9 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter
|
||||
if (program == null) {
|
||||
return;
|
||||
}
|
||||
long curLifeStart = module.getObject().getLife().spanContaining(snap).min();
|
||||
ModuleMapProposal proposal =
|
||||
staticMappingService.proposeModuleMap(module, current.getSnap(), program);
|
||||
staticMappingService.proposeModuleMap(module, curLifeStart, program);
|
||||
Map<TraceModule, ModuleMapEntry> map = proposal.computeMap();
|
||||
promptModuleProposal(map.values(), NO_MODULES_PROPOSAL_SEL);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import docking.action.DockingActionIf;
|
||||
import docking.action.builder.ActionBuilder;
|
||||
import generic.ULongSpan;
|
||||
import generic.theme.GThemeDefaults.Colors;
|
||||
import ghidra.app.plugin.core.debug.gui.breakpoint.timeline.BreakpointTimelineProvider;
|
||||
import ghidra.app.plugin.core.debug.gui.timeoverview.*;
|
||||
import ghidra.app.plugin.core.overview.OverviewColorLegendDialog;
|
||||
import ghidra.app.services.DebuggerTraceManagerService;
|
||||
@@ -38,7 +39,6 @@ import ghidra.program.model.symbol.RefType;
|
||||
import ghidra.trace.model.*;
|
||||
import ghidra.trace.model.breakpoint.*;
|
||||
import ghidra.trace.model.breakpoint.TraceBreakpointKind.TraceBreakpointKindSet;
|
||||
import ghidra.trace.model.stack.TraceStackFrame;
|
||||
import ghidra.trace.model.symbol.TraceReference;
|
||||
import ghidra.trace.model.target.TraceObjectValue;
|
||||
import ghidra.trace.util.TraceEvents;
|
||||
@@ -86,27 +86,21 @@ public class BreakpointTimeOverviewColorService implements TimeOverviewColorServ
|
||||
}
|
||||
|
||||
private static String OPTIONS_NAME = "Breakpoint Hit Timeline";
|
||||
|
||||
private PluginTool tool;
|
||||
|
||||
Trace currentTrace;
|
||||
TimeOverviewColorComponent overviewComponent;
|
||||
DialogComponentProvider legendDialog;
|
||||
BreakTypeOverviewLegendPanel legendPanel;
|
||||
TimeOverviewColorPlugin plugin;
|
||||
|
||||
private final BreakpointTimeOverviewEventListener eventListener =
|
||||
new BreakpointTimeOverviewEventListener();
|
||||
|
||||
private final Map<CellType, Color> colorMap = new HashMap<>();
|
||||
private final Map<Integer, Long> indexToSnap = new HashMap<>();
|
||||
private final Map<Long, Color> snapToColor = new HashMap<>();
|
||||
private final Map<Long, String> snapToTooltip = new HashMap<>();
|
||||
private final Map<Long, ULongSpan> snapToRange = new HashMap<>();
|
||||
|
||||
Trace currentTrace;
|
||||
TimeOverviewColorComponent overviewComponent;
|
||||
DialogComponentProvider legendDialog;
|
||||
BreakTypeOverviewLegendPanel legendPanel;
|
||||
TimeOverviewColorPlugin plugin;
|
||||
List<BreakpointEvent> snapsWithBreakpointsHit = new ArrayList<>();
|
||||
|
||||
Lifespan bounds;
|
||||
private PluginTool tool;
|
||||
private DebuggerTraceManagerService debuggerTraceManagerService;
|
||||
|
||||
protected void calculateBreakpointHits() {
|
||||
@@ -192,9 +186,11 @@ public class BreakpointTimeOverviewColorService implements TimeOverviewColorServ
|
||||
|
||||
private void findAndAddExecuteBreakpointHits(TraceBreakpointLocation breakpointLocation,
|
||||
AddressRange range) {
|
||||
final Collection<? extends TraceObjectValue> intersecting = currentTrace.getObjectManager()
|
||||
.getValuesIntersecting(Lifespan.ALL, range, TraceStackFrame.KEY_PC);
|
||||
for (final TraceObjectValue tov : intersecting) {
|
||||
final Iterator<? extends TraceObjectValue> intersecting = BreakpointTimelineProvider
|
||||
.getTraceObjectValuesWithPCsIntersectingRange(currentTrace, range);
|
||||
|
||||
while (intersecting.hasNext()) {
|
||||
final TraceObjectValue tov = intersecting.next();
|
||||
snapsWithBreakpointsHit
|
||||
.add(new BreakpointEvent(tov.getMinSnap(), CellType.INSTRUCTION_EXECUTED));
|
||||
}
|
||||
@@ -240,7 +236,8 @@ public class BreakpointTimeOverviewColorService implements TimeOverviewColorServ
|
||||
/**
|
||||
* Returns the color associated with the given {@link CellType}
|
||||
*
|
||||
* @param breakType the span type for which to get a color.
|
||||
* @param breakType
|
||||
* the span type for which to get a color.
|
||||
* @return the color associated with the given {@link CellType}
|
||||
*/
|
||||
public Color getColor(CellType breakType) {
|
||||
@@ -358,4 +355,4 @@ public class BreakpointTimeOverviewColorService implements TimeOverviewColorServ
|
||||
bounds = Lifespan.span(0, getTrace().getTimeManager().getMaxSnap());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
/* ###
|
||||
* 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 docking.widgets.gtreetable.GTreeTableNode;
|
||||
import ghidra.trace.model.time.TraceSnapshot;
|
||||
import ghidra.util.NumericUtilities;
|
||||
import resources.Icons;
|
||||
|
||||
abstract class AbstractTraceCallTreeNode extends GTreeTableNode {
|
||||
public record ParamNameToBytes(String name, byte[] bytes) {}
|
||||
|
||||
private final TraceSnapshot snap;
|
||||
private final List<ParamNameToBytes> parameters;
|
||||
private final byte[] returnVal;
|
||||
private final String module;
|
||||
private int largestParamSize = 0;
|
||||
|
||||
public AbstractTraceCallTreeNode(final String name, final String module,
|
||||
final TraceSnapshot snap, List<ParamNameToBytes> parameters, final byte[] returnVal) {
|
||||
super(name);
|
||||
icon = Icons.RIGHT_ICON;
|
||||
this.snap = snap;
|
||||
this.parameters = parameters;
|
||||
this.returnVal = returnVal;
|
||||
this.module = module;
|
||||
}
|
||||
|
||||
public int getLargestParamSize() {
|
||||
return largestParamSize;
|
||||
}
|
||||
|
||||
public String getModule() {
|
||||
return module;
|
||||
}
|
||||
|
||||
public ParamNameToBytes getParameter(int i) {
|
||||
if ((parameters != null) && (i >= 0) && (i < parameters.size())) {
|
||||
return parameters.get(i);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getParameterNumber() {
|
||||
return parameters.size();
|
||||
}
|
||||
|
||||
public List<ParamNameToBytes> getParameters() {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
public String getParameterString(int i) {
|
||||
if ((parameters != null) && (i >= 0) && (i < parameters.size())) {
|
||||
return "%s: %s".formatted(parameters.get(i).name,
|
||||
NumericUtilities.convertBytesToString(parameters.get(i).bytes));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public byte[] getReturnVal() {
|
||||
return returnVal;
|
||||
}
|
||||
|
||||
public String getReturnValString() {
|
||||
if (returnVal == null) {
|
||||
return "";
|
||||
}
|
||||
return "Return: %s".formatted(NumericUtilities.convertBytesToString(returnVal));
|
||||
}
|
||||
|
||||
public long getSnapshotKey() {
|
||||
if (snap == null) {
|
||||
return -1;
|
||||
}
|
||||
return snap.getKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTreeData() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setLargestParamSize(int largestParamSize) {
|
||||
this.largestParamSize = largestParamSize;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/* ###
|
||||
* 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 docking.DefaultActionContext;
|
||||
import docking.widgets.table.GTable;
|
||||
|
||||
public class TraceCallTreeActionContext extends DefaultActionContext {
|
||||
private final List<AbstractTraceCallTreeNode> selected;
|
||||
|
||||
public TraceCallTreeActionContext(TraceCallTreeProvider provider,
|
||||
List<AbstractTraceCallTreeNode> selected, GTable source) {
|
||||
super(provider, selected, source);
|
||||
this.selected = selected;
|
||||
}
|
||||
|
||||
public List<AbstractTraceCallTreeNode> getSelected() {
|
||||
return selected;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/* ###
|
||||
* 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;
|
||||
|
||||
public class TraceCallTreeCallNode extends AbstractTraceCallTreeNode {
|
||||
public TraceCallTreeCallNode(String name, String module, TraceSnapshot snap,
|
||||
List<ParamNameToBytes> parameters, byte[] returnVal) {
|
||||
super(name, module, snap, parameters, returnVal);
|
||||
}
|
||||
}
|
||||
@@ -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 TraceCallTreeExternalNode extends AbstractTraceCallTreeNode {
|
||||
public TraceCallTreeExternalNode(String name, String module, TraceSnapshot snap,
|
||||
List<ParamNameToBytes> parameters, byte[] returnVal) {
|
||||
super(name, module, snap, parameters, returnVal);
|
||||
icon = Icons.STRONG_WARNING_ICON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTreeData() {
|
||||
return "External: %s".formatted(name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/* ###
|
||||
* 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.Objects;
|
||||
|
||||
import docking.DefaultActionContext;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.trace.model.Trace;
|
||||
|
||||
public class TraceCallTreeLogContext extends DefaultActionContext {
|
||||
private final Trace trace;
|
||||
private final Address dynamicPC;
|
||||
private final int hashCode;
|
||||
|
||||
public TraceCallTreeLogContext(TraceCallTreeProvider provider, Trace trace, Address dynamicPC) {
|
||||
super(provider);
|
||||
this.trace = trace;
|
||||
this.dynamicPC = dynamicPC;
|
||||
hashCode = Objects.hash(getClass(), trace, dynamicPC);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (!(obj instanceof TraceCallTreeLogContext that)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Objects.equals(this.trace, that.trace)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Objects.equals(this.dynamicPC, that.dynamicPC)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hashCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/* ###
|
||||
* 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.HashMap;
|
||||
import java.util.Objects;
|
||||
|
||||
import docking.widgets.table.AbstractDynamicTableColumn;
|
||||
import docking.widgets.table.TableColumnDescriptor;
|
||||
import docking.widgets.table.threaded.ThreadedTableModelStub;
|
||||
import ghidra.docking.settings.Settings;
|
||||
import ghidra.framework.plugintool.ServiceProvider;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.trace.model.Trace;
|
||||
import ghidra.util.datastruct.Accumulator;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
public class TraceCallTreeLogModel
|
||||
extends ThreadedTableModelStub<TraceCallTreeLogModel.TraceCallTreeLogObject> {
|
||||
private static class DynamicAddressColumn
|
||||
extends AbstractDynamicTableColumn<TraceCallTreeLogObject, Address, Object> {
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return "Dynamic Address";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getValue(TraceCallTreeLogObject rowObject, Settings settings, Object data,
|
||||
ServiceProvider services) throws IllegalArgumentException {
|
||||
return rowObject.dynamicAddress();
|
||||
}
|
||||
}
|
||||
|
||||
private static class MessageColumn
|
||||
extends AbstractDynamicTableColumn<TraceCallTreeLogObject, String, Object> {
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return "Message";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue(TraceCallTreeLogObject rowObject, Settings settings, Object data,
|
||||
ServiceProvider services) throws IllegalArgumentException {
|
||||
return rowObject.log();
|
||||
}
|
||||
}
|
||||
|
||||
private static class SnapColumn
|
||||
extends AbstractDynamicTableColumn<TraceCallTreeLogObject, Long, Object> {
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return "Snap";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getValue(TraceCallTreeLogObject rowObject, Settings settings, Object data,
|
||||
ServiceProvider services) throws IllegalArgumentException {
|
||||
return rowObject.snap();
|
||||
}
|
||||
}
|
||||
|
||||
private static class StaticAddressColumn
|
||||
extends AbstractDynamicTableColumn<TraceCallTreeLogObject, Address, Object> {
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return "Static Address";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getValue(TraceCallTreeLogObject rowObject, Settings settings, Object data,
|
||||
ServiceProvider services) throws IllegalArgumentException {
|
||||
return rowObject.staticAddress();
|
||||
}
|
||||
}
|
||||
|
||||
public record TraceCallTreeLogObject(Trace trace, String log, long snap, Address dynamicAddress,
|
||||
Address staticAddress) {
|
||||
public TraceCallTreeLogObject {
|
||||
Objects.requireNonNull(log);
|
||||
}
|
||||
}
|
||||
|
||||
private static class TraceColumn
|
||||
extends AbstractDynamicTableColumn<TraceCallTreeLogObject, String, Object> {
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return "Trace";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue(TraceCallTreeLogObject rowObject, Settings settings, Object data,
|
||||
ServiceProvider services) throws IllegalArgumentException {
|
||||
return rowObject.trace().getName();
|
||||
}
|
||||
}
|
||||
|
||||
record LogKey(String traceName, Address dynamicAddress, long snap) {}
|
||||
|
||||
HashMap<LogKey, TraceCallTreeLogObject> logs;
|
||||
|
||||
protected TraceCallTreeLogModel(TraceCallTreePlugin plugin) {
|
||||
super("Trace Call Tree Log Model", plugin.getTool());
|
||||
logs = new HashMap<>();
|
||||
}
|
||||
|
||||
void clear() {
|
||||
logs.clear();
|
||||
reload();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TableColumnDescriptor<TraceCallTreeLogObject> createTableColumnDescriptor() {
|
||||
final TableColumnDescriptor<TraceCallTreeLogObject> descriptor =
|
||||
new TableColumnDescriptor<>();
|
||||
descriptor.addVisibleColumn(new TraceColumn());
|
||||
descriptor.addVisibleColumn(new SnapColumn());
|
||||
descriptor.addVisibleColumn(new MessageColumn());
|
||||
descriptor.addVisibleColumn(new DynamicAddressColumn());
|
||||
descriptor.addVisibleColumn(new StaticAddressColumn());
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoad(Accumulator<TraceCallTreeLogObject> accumulator, TaskMonitor monitor)
|
||||
throws CancelledException {
|
||||
accumulator.addAll(logs.values());
|
||||
}
|
||||
|
||||
void log(Trace trace, String msg, long snap, Address staticAddress, Address dynamicAddress) {
|
||||
final TraceCallTreeLogObject log =
|
||||
new TraceCallTreeLogObject(trace, msg, snap, dynamicAddress, staticAddress);
|
||||
final LogKey key = new LogKey(trace.getName(), dynamicAddress, snap);
|
||||
logs.remove(key);
|
||||
logs.put(key, log);
|
||||
reload();
|
||||
}
|
||||
|
||||
void resolve(Trace trace, long snap, Address dynamicAddress) {
|
||||
final LogKey key = new LogKey(trace.getName(), dynamicAddress, snap);
|
||||
logs.remove(key);
|
||||
reload();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/* ###
|
||||
* 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.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import docking.widgets.gtreetable.GTreeTableModel;
|
||||
import docking.widgets.table.*;
|
||||
import ghidra.docking.settings.Settings;
|
||||
import ghidra.framework.plugintool.ServiceProvider;
|
||||
|
||||
class TraceCallTreeModel extends GTreeTableModel<AbstractTraceCallTreeNode> {
|
||||
|
||||
private static class ModuleColumn
|
||||
extends AbstractDynamicTableColumnStub<AbstractTraceCallTreeNode, String> {
|
||||
private final String columnName;
|
||||
|
||||
ModuleColumn(String columnName) {
|
||||
this.columnName = columnName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return columnName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue(AbstractTraceCallTreeNode rowObject, Settings settings,
|
||||
ServiceProvider provider) {
|
||||
return rowObject.getModule();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class ParameterColumn
|
||||
extends AbstractDynamicTableColumnStub<AbstractTraceCallTreeNode, String> {
|
||||
private final int paramNum;
|
||||
|
||||
ParameterColumn(int paramNum) {
|
||||
this.paramNum = paramNum;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return "Parameter %d".formatted(paramNum);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue(AbstractTraceCallTreeNode rowObject, Settings settings,
|
||||
ServiceProvider provider) {
|
||||
return rowObject.getParameterString(paramNum);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ReturnColumn
|
||||
extends AbstractDynamicTableColumnStub<AbstractTraceCallTreeNode, String> {
|
||||
private final String columnName;
|
||||
|
||||
ReturnColumn(String columnName) {
|
||||
this.columnName = columnName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return columnName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue(AbstractTraceCallTreeNode rowObject, Settings settings,
|
||||
ServiceProvider provider) {
|
||||
return rowObject.getReturnValString();
|
||||
}
|
||||
}
|
||||
|
||||
private static class SnapColumn
|
||||
extends AbstractDynamicTableColumnStub<AbstractTraceCallTreeNode, Long> {
|
||||
private final String columnName;
|
||||
|
||||
SnapColumn(String columnName) {
|
||||
this.columnName = columnName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return columnName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getValue(AbstractTraceCallTreeNode rowObject, Settings settings,
|
||||
ServiceProvider provider) {
|
||||
return rowObject.getSnapshotKey();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Set<DynamicTableColumn<AbstractTraceCallTreeNode, ?, ?>> parameterColumns = new HashSet<>();
|
||||
|
||||
public TraceCallTreeModel(AbstractTraceCallTreeNode rootNode) {
|
||||
super(rootNode);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TableColumnDescriptor<AbstractTraceCallTreeNode> createTableColumnDescriptor() {
|
||||
final TableColumnDescriptor<AbstractTraceCallTreeNode> descriptor =
|
||||
super.createTableColumnDescriptor();
|
||||
descriptor.addVisibleColumn(new SnapColumn("Snapshot"));
|
||||
descriptor.addVisibleColumn(new ModuleColumn("Module"));
|
||||
descriptor.addVisibleColumn(new ReturnColumn("Return"));
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public void setNumberOfParameterColumns(int num) {
|
||||
removeTableColumns(parameterColumns);
|
||||
parameterColumns.clear();
|
||||
for (int i = 0; i < num; i++) {
|
||||
final ParameterColumn column = new ParameterColumn(i);
|
||||
parameterColumns.add(column);
|
||||
}
|
||||
if (!parameterColumns.isEmpty()) {
|
||||
addTableColumns(parameterColumns);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String treeColumnName() {
|
||||
return "Function";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/* ###
|
||||
* 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 ghidra.app.events.CloseProgramPluginEvent;
|
||||
import ghidra.app.events.OpenProgramPluginEvent;
|
||||
import ghidra.app.plugin.PluginCategoryNames;
|
||||
import ghidra.app.plugin.core.debug.AbstractDebuggerPlugin;
|
||||
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
|
||||
import ghidra.app.plugin.core.debug.event.TraceActivatedPluginEvent;
|
||||
import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent;
|
||||
import ghidra.app.services.*;
|
||||
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
|
||||
import ghidra.framework.plugintool.*;
|
||||
import ghidra.framework.plugintool.util.PluginStatus;
|
||||
import ghidra.trace.model.Trace;
|
||||
|
||||
@PluginInfo(
|
||||
shortDescription = "Call Tree Viewer For Traces",
|
||||
description = "Displays a call tree of all functions ran in a trace",
|
||||
category = PluginCategoryNames.DEBUGGER,
|
||||
packageName = DebuggerPluginPackage.NAME,
|
||||
status = PluginStatus.UNSTABLE,
|
||||
servicesRequired = { DebuggerTraceManagerService.class, DebuggerStaticMappingService.class,
|
||||
DebuggerListingService.class, ProgressService.class },
|
||||
eventsConsumed = { TraceClosedPluginEvent.class, TraceActivatedPluginEvent.class,
|
||||
OpenProgramPluginEvent.class, CloseProgramPluginEvent.class }
|
||||
)
|
||||
public class TraceCallTreePlugin extends AbstractDebuggerPlugin {
|
||||
TraceCallTreeProvider provider;
|
||||
|
||||
private DebuggerCoordinates current;
|
||||
|
||||
public TraceCallTreePlugin(PluginTool tool) {
|
||||
super(tool);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dispose() {
|
||||
tool.removeComponentProvider(provider);
|
||||
provider.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
super.init();
|
||||
provider = new TraceCallTreeProvider(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processEvent(PluginEvent event) {
|
||||
super.processEvent(event);
|
||||
switch (event) {
|
||||
case final TraceClosedPluginEvent evt -> {
|
||||
final Trace t = evt.getTrace();
|
||||
if ((current != null) && (current.getTrace() == t)) {
|
||||
current = null;
|
||||
provider.setCoordinates(null);
|
||||
}
|
||||
provider.traceClosed(evt.getTrace());
|
||||
}
|
||||
case final TraceActivatedPluginEvent evt -> {
|
||||
current = evt.getActiveCoordinates();
|
||||
provider.setCoordinates(current);
|
||||
}
|
||||
case final OpenProgramPluginEvent evt -> {
|
||||
provider.programOpened(evt.getProgram());
|
||||
}
|
||||
case final CloseProgramPluginEvent evt -> {
|
||||
provider.programClosed(evt.getProgram());
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<ParamNameToBytes> parameters, byte[] returnVal) {
|
||||
super(name, module, snap, parameters, returnVal);
|
||||
icon = Icons.ARROW_UP_LEFT_ICON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTreeData() {
|
||||
return "Return: %s".formatted(name);
|
||||
}
|
||||
}
|
||||
@@ -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<AbstractTraceCallTreeNode> {
|
||||
private MessageOverlay msgOverlay;
|
||||
|
||||
public TraceCallTreeTable(GTreeTableModel<AbstractTraceCallTreeNode> model) {
|
||||
super(model);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addTableToPanel(GTable table) {
|
||||
msgOverlay = new MessageOverlay();
|
||||
final JScrollPane pane = new JScrollPane(table);
|
||||
final JLayer<JComponent> layer = new JLayer<>(pane, msgOverlay);
|
||||
add(layer);
|
||||
}
|
||||
|
||||
public void setStatusMessage(String msg) {
|
||||
msgOverlay.setMessage(msg);
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
@@ -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<ParamNameToBytes> 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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
"<html>The module <b><tt>" +
|
||||
HTMLUtilities.escapeHTML(module.getName(firstSnap)) +
|
||||
"</tt></b> was not found in the project</html>",
|
||||
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<TraceModule, ModuleMapEntry> 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,
|
||||
"<html>Program <b><tt>" +
|
||||
HTMLUtilities.escapeHTML(match.getPathname()) +
|
||||
"</tt></b> is used by " + trace.getName() +
|
||||
". Open it so it can mapped.</html>",
|
||||
new DebuggerOpenProgramActionContext(match));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkAllTraceMappingsForProgram(Program program, TaskMonitor monitor)
|
||||
throws CancelledException {
|
||||
Iterator<Trace> 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<TraceModule, ModuleMapEntry> 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);
|
||||
|
||||
@@ -703,7 +703,7 @@ public class DynamicStaticSynchronizationPlugin extends Plugin {
|
||||
consoleService.log(DebuggerResources.ICON_LOG_ERROR,
|
||||
"<html>The module <b><tt>" + HTMLUtilities.escapeHTML(mod.getName(snap)) +
|
||||
"</tt></b> was not found in the project</html>",
|
||||
new DebuggerMissingModuleActionContext(mod));
|
||||
new DebuggerMissingModuleActionContext(mod, snap));
|
||||
}
|
||||
/**
|
||||
* Once the programs are opened, including those which are successfully imported, the
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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<JComponent> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<T extends GTreeTableNode> extends JPanel {
|
||||
private final GTable table;
|
||||
private final GTableFilterPanel<T> filterPanel;
|
||||
|
||||
private final GTreeTableModel<T> tableModel;
|
||||
|
||||
public GTreeTable(GTreeTableModel<T> 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<T> 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<T> getTableModel() {
|
||||
return tableModel;
|
||||
}
|
||||
}
|
||||
@@ -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<T extends GTreeTableNode> extends GTableCellRenderer
|
||||
implements GColumnRenderer<T> {
|
||||
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)));
|
||||
}
|
||||
}
|
||||
@@ -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<T extends GTreeTableNode> extends ThreadedTableModelStub<T> {
|
||||
protected class TreeColumn extends AbstractDynamicTableColumnStub<T, GTreeTableNode> {
|
||||
private final String columnName;
|
||||
private final GTreeTableCellRenderer<GTreeTableNode> renderer;
|
||||
|
||||
public TreeColumn(String columnName) {
|
||||
this.columnName = columnName;
|
||||
renderer = new GTreeTableCellRenderer<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return columnName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GColumnRenderer<GTreeTableNode> 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
|
||||
* <p>
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected TableColumnDescriptor<T> createTableColumnDescriptor() {
|
||||
final TableColumnDescriptor<T> 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
|
||||
* <p>
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected List<T> doFilter(List<T> data, TableSortingContext<T> lastSortingContext,
|
||||
TaskMonitor monitor) throws CancelledException {
|
||||
if ((data.isEmpty()) || !hasFilter()) {
|
||||
return data;
|
||||
}
|
||||
|
||||
final TableFilter<T> filterCopy = getTableFilter();
|
||||
final Set<T> filteredList = new HashSet<>();
|
||||
final List<T> 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<T> 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";
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
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<GTreeTableNode> {
|
||||
final List<GTreeTableNode> 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<GTreeTableNode> {
|
||||
final List<GTreeTableNode> 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<GTreeTableNode> 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<GTreeTableNode> 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<GTreeTableNode> depthFirstSearchList() {
|
||||
final List<GTreeTableNode> nodes = new LinkedList<>();
|
||||
final List<GTreeTableNode> 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<GTreeTableNode> descendants() {
|
||||
return EachDecendantIterator::new;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an iterable of all the expanded descendants of this node
|
||||
*
|
||||
* @return An iterable of expanded descendants
|
||||
*/
|
||||
public Iterable<GTreeTableNode> 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<GTreeTableNode> find(Predicate<GTreeTableNode> condition) {
|
||||
final List<GTreeTableNode> nodes = new LinkedList<>();
|
||||
final List<GTreeTableNode> 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<GTreeTableNode> 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<GTreeTableNode> action) {
|
||||
final List<GTreeTableNode> 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<GTreeTableNode> action) {
|
||||
final List<GTreeTableNode> 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<GTreeTableNode> getChildren() {
|
||||
return children;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of descendants who are expanded
|
||||
*
|
||||
* @return List of expanded descendants
|
||||
*/
|
||||
public List<GTreeTableNode> getExpanded() {
|
||||
final List<GTreeTableNode> 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<GTreeTableNode> getPath() {
|
||||
final List<GTreeTableNode> 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user