mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2026-05-24 11:25:31 +08:00
Merge remote-tracking branch 'origin/Ghidra_11.1'
This commit is contained in:
@@ -53,7 +53,8 @@
|
||||
<attribute-alias from="_kinds" to="Kinds" />
|
||||
<attribute name="_display" schema="STRING" hidden="yes" />
|
||||
<attribute name="_order" schema="INT" hidden="yes" />
|
||||
<attribute name="_enabled" schema="BOOL" required="yes" hidden="yes" />
|
||||
<attribute name="Enabled" schema="BOOL" />
|
||||
<attribute-alias from="_enabled" to="Enabled" />
|
||||
<attribute name="Commands" schema="STRING" />
|
||||
<attribute name="Condition" schema="STRING" />
|
||||
<attribute name="Hit Count" schema="INT" />
|
||||
@@ -132,6 +133,8 @@
|
||||
<schema name="BreakpointLocation" elementResync="NEVER" attributeResync="NEVER">
|
||||
<interface name="BreakpointLocation" />
|
||||
<element schema="VOID" />
|
||||
<attribute name="Enabled" schema="BOOL" />
|
||||
<attribute-alias from="_enabled" to="Enabled" />
|
||||
<attribute name="Range" schema="RANGE" />
|
||||
<attribute-alias from="_range" to="Range" />
|
||||
<attribute name="_display" schema="STRING" hidden="yes" />
|
||||
|
||||
+2
-3
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.app.plugin.core.debug.gui.modules;
|
||||
package ghidra.debug.api.modules;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -43,10 +43,9 @@ public class DebuggerMissingModuleActionContext extends DefaultActionContext {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (!(obj instanceof DebuggerMissingModuleActionContext)) {
|
||||
if (!(obj instanceof DebuggerMissingModuleActionContext that)) {
|
||||
return false;
|
||||
}
|
||||
DebuggerMissingModuleActionContext that = (DebuggerMissingModuleActionContext) obj;
|
||||
if (!this.module.equals(that.module)) {
|
||||
return false;
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/* ###
|
||||
* 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.modules;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import docking.DefaultActionContext;
|
||||
import ghidra.program.model.address.*;
|
||||
import ghidra.program.model.listing.InstructionIterator;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.trace.model.Trace;
|
||||
|
||||
public class DebuggerMissingProgramActionContext extends DefaultActionContext {
|
||||
|
||||
public static Address getMappingProbeAddress(Program program) {
|
||||
if (program == null) {
|
||||
return null;
|
||||
}
|
||||
AddressIterator eepi = program.getSymbolTable().getExternalEntryPointIterator();
|
||||
if (eepi.hasNext()) {
|
||||
return eepi.next();
|
||||
}
|
||||
InstructionIterator ii = program.getListing().getInstructions(true);
|
||||
if (ii.hasNext()) {
|
||||
return ii.next().getAddress();
|
||||
}
|
||||
AddressSetView es = program.getMemory().getExecuteSet();
|
||||
if (!es.isEmpty()) {
|
||||
return es.getMinAddress();
|
||||
}
|
||||
if (!program.getMemory().isEmpty()) {
|
||||
return program.getMinAddress();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private final Trace trace;
|
||||
private final Program program;
|
||||
private final int hashCode;
|
||||
|
||||
private Address probe;
|
||||
|
||||
public DebuggerMissingProgramActionContext(Trace trace, Program program) {
|
||||
this.trace = Objects.requireNonNull(trace);
|
||||
this.program = Objects.requireNonNull(program);
|
||||
this.hashCode = Objects.hash(getClass(), trace, program);
|
||||
}
|
||||
|
||||
public Trace getTrace() {
|
||||
return trace;
|
||||
}
|
||||
|
||||
public Program getProgram() {
|
||||
return program;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (!(obj instanceof DebuggerMissingProgramActionContext that)) {
|
||||
return false;
|
||||
}
|
||||
if (!this.trace.equals(that.trace)) {
|
||||
return false;
|
||||
}
|
||||
if (!this.program.equals(that.program)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public Address getMappingProbeAddress() {
|
||||
if (probe == null) {
|
||||
probe = getMappingProbeAddress(program);
|
||||
}
|
||||
return probe;
|
||||
}
|
||||
}
|
||||
+46
-2
@@ -17,15 +17,39 @@ package ghidra.debug.api.tracermi;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import ghidra.app.services.Terminal;
|
||||
|
||||
/**
|
||||
* A terminal with some back-end element attached to it
|
||||
*/
|
||||
public interface TerminalSession extends AutoCloseable {
|
||||
@Override
|
||||
void close() throws IOException;
|
||||
default void close() throws IOException {
|
||||
terminate();
|
||||
terminal().close();
|
||||
}
|
||||
|
||||
/**
|
||||
* The handle to the terminal
|
||||
*
|
||||
* @return the handle
|
||||
*/
|
||||
Terminal terminal();
|
||||
|
||||
/**
|
||||
* Ensure the session is visible
|
||||
*
|
||||
* <p>
|
||||
* The window should be displayed and brought to the front.
|
||||
*/
|
||||
default void show() {
|
||||
terminal().toFront();
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate the session without closing the terminal
|
||||
*
|
||||
* @throws IOException if an I/O issue occurs during termination
|
||||
*/
|
||||
void terminate() throws IOException;
|
||||
|
||||
@@ -34,7 +58,9 @@ public interface TerminalSession extends AutoCloseable {
|
||||
*
|
||||
* @return true for terminated, false for active
|
||||
*/
|
||||
boolean isTerminated();
|
||||
default boolean isTerminated() {
|
||||
return terminal().isTerminated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a human-readable description of the session
|
||||
@@ -42,4 +68,22 @@ public interface TerminalSession extends AutoCloseable {
|
||||
* @return the description
|
||||
*/
|
||||
String description();
|
||||
|
||||
/**
|
||||
* Get the terminal contents as a string (no attributes)
|
||||
*
|
||||
* @return the content
|
||||
*/
|
||||
default String content() {
|
||||
return terminal().getFullText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current title of the terminal
|
||||
*
|
||||
* @return the title
|
||||
*/
|
||||
default String title() {
|
||||
return terminal().getSubTitle();
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -71,6 +71,12 @@ public interface TraceRmiLaunchOffer {
|
||||
this.exception = exception;
|
||||
}
|
||||
|
||||
public void showTerminals() {
|
||||
for (TerminalSession session : sessions.values()) {
|
||||
session.show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
if (connection != null) {
|
||||
|
||||
+21
-45
@@ -45,16 +45,14 @@ import ghidra.framework.options.SaveState;
|
||||
import ghidra.framework.plugintool.AutoConfigState.ConfigStateField;
|
||||
import ghidra.framework.plugintool.AutoConfigState.PathIsFile;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.program.model.address.*;
|
||||
import ghidra.program.model.listing.InstructionIterator;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.program.util.ProgramLocation;
|
||||
import ghidra.pty.*;
|
||||
import ghidra.trace.model.Trace;
|
||||
import ghidra.trace.model.TraceLocation;
|
||||
import ghidra.trace.model.modules.TraceModule;
|
||||
import ghidra.util.MessageType;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.*;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.task.Task;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
@@ -67,12 +65,6 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
|
||||
|
||||
protected record PtyTerminalSession(Terminal terminal, Pty pty, PtySession session,
|
||||
Thread waiter) implements TerminalSession {
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
terminate();
|
||||
terminal.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminate() throws IOException {
|
||||
terminal.terminated();
|
||||
@@ -81,11 +73,6 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
|
||||
waiter.interrupt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTerminated() {
|
||||
return terminal.isTerminated();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return session.description();
|
||||
@@ -94,23 +81,12 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
|
||||
|
||||
protected record NullPtyTerminalSession(Terminal terminal, Pty pty, String name)
|
||||
implements TerminalSession {
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
terminate();
|
||||
terminal.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminate() throws IOException {
|
||||
terminal.terminated();
|
||||
pty.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTerminated() {
|
||||
return terminal.isTerminated();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return name;
|
||||
@@ -171,25 +147,8 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
|
||||
}
|
||||
|
||||
protected Address getMappingProbeAddress() {
|
||||
if (program == null) {
|
||||
return null;
|
||||
}
|
||||
AddressIterator eepi = program.getSymbolTable().getExternalEntryPointIterator();
|
||||
if (eepi.hasNext()) {
|
||||
return eepi.next();
|
||||
}
|
||||
InstructionIterator ii = program.getListing().getInstructions(true);
|
||||
if (ii.hasNext()) {
|
||||
return ii.next().getAddress();
|
||||
}
|
||||
AddressSetView es = program.getMemory().getExecuteSet();
|
||||
if (!es.isEmpty()) {
|
||||
return es.getMinAddress();
|
||||
}
|
||||
if (!program.getMemory().isEmpty()) {
|
||||
return program.getMinAddress();
|
||||
}
|
||||
return null; // I guess we won't wait for a mapping, then
|
||||
// May be null, in which case, we won't wait for a mapping
|
||||
return DebuggerMissingProgramActionContext.getMappingProbeAddress(program);
|
||||
}
|
||||
|
||||
protected CompletableFuture<Void> listenForMapping(DebuggerStaticMappingService mappingService,
|
||||
@@ -686,6 +645,22 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
|
||||
}
|
||||
return new LaunchResult(program, Map.of(), null, null, null, lastExc);
|
||||
}
|
||||
catch (NoStaticMappingException e) {
|
||||
DebuggerConsoleService consoleService =
|
||||
tool.getService(DebuggerConsoleService.class);
|
||||
if (consoleService == null) {
|
||||
Msg.error(this, e.getMessage());
|
||||
}
|
||||
else {
|
||||
consoleService.log(DebuggerResources.ICON_MODULES,
|
||||
"<html>The trace <b>%s</b> has no mapping to its program <b>%s</b></html>"
|
||||
.formatted(
|
||||
HTMLUtilities.escapeHTML(trace.getDomainFile().getName()),
|
||||
HTMLUtilities.escapeHTML(program.getDomainFile().getName())),
|
||||
new DebuggerMissingProgramActionContext(trace, program));
|
||||
}
|
||||
return new LaunchResult(program, sessions, acceptor, connection, trace, e);
|
||||
}
|
||||
catch (Exception e) {
|
||||
DebuggerConsoleService consoleService =
|
||||
tool.getService(DebuggerConsoleService.class);
|
||||
@@ -700,6 +675,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
|
||||
if (prompt) {
|
||||
switch (promptError(result)) {
|
||||
case KEEP:
|
||||
result.showTerminals();
|
||||
return result;
|
||||
case RETRY:
|
||||
try {
|
||||
|
||||
+27
-1
@@ -15,7 +15,10 @@
|
||||
*/
|
||||
package ghidra.app.plugin.core.debug.gui.tracermi.launcher;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import docking.widgets.OptionDialog;
|
||||
import ghidra.debug.api.tracermi.TerminalSession;
|
||||
@@ -26,7 +29,7 @@ import ghidra.util.HelpLocation;
|
||||
public class LaunchFailureDialog extends OptionDialog {
|
||||
private static final String MSGPAT_PART_TOP = """
|
||||
<html><body width="400px">
|
||||
<h3>Failed to launch %s due to an exception:</h3>
|
||||
<h3>Failed to launch '%s' due to an exception:</h3>
|
||||
|
||||
<tt>%s</tt>
|
||||
|
||||
@@ -56,6 +59,7 @@ public class LaunchFailureDialog extends OptionDialog {
|
||||
""";
|
||||
private static final String MSGPAT_WITH_RESOURCES = MSGPAT_PART_TOP + MSGPAT_PART_RESOURCES;
|
||||
private static final String MSGPAT_WITHOUT_RESOURCES = MSGPAT_PART_TOP;
|
||||
private static final int MAX_TERM_CONTENT_LINES = 2;
|
||||
|
||||
public enum ErrPromptResponse {
|
||||
KEEP, RETRY, TERMINATE;
|
||||
@@ -90,6 +94,25 @@ public class LaunchFailureDialog extends OptionDialog {
|
||||
result.trace() != null;
|
||||
}
|
||||
|
||||
protected static String htmlContent(TerminalSession session) {
|
||||
String content = session.content().trim();
|
||||
List<String> lines = Arrays.asList(content.split("\n"));
|
||||
String note = "";
|
||||
if (lines.size() >= MAX_TERM_CONTENT_LINES) {
|
||||
note = "(last %d lines)".formatted(MAX_TERM_CONTENT_LINES);
|
||||
content = lines.subList(lines.size() - MAX_TERM_CONTENT_LINES, lines.size())
|
||||
.stream()
|
||||
.collect(Collectors.joining("\n"));
|
||||
}
|
||||
return """
|
||||
<div style="font:bold;">Title: %s</div>%s
|
||||
<div style="background:black;color:white;">
|
||||
<pre>%s</pre>""".formatted(
|
||||
session.title(),
|
||||
note,
|
||||
content);
|
||||
}
|
||||
|
||||
protected static String htmlResources(LaunchResult result) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Entry<String, TerminalSession> ent : result.sessions().entrySet()) {
|
||||
@@ -100,6 +123,9 @@ public class LaunchFailureDialog extends OptionDialog {
|
||||
if (session.isTerminated()) {
|
||||
sb.append(" (Terminated)");
|
||||
}
|
||||
sb.append("<div>\n");
|
||||
sb.append(htmlContent(session));
|
||||
sb.append("</div>");
|
||||
sb.append("</li>\n");
|
||||
}
|
||||
if (result.acceptor() != null) {
|
||||
|
||||
+48
-8
@@ -152,14 +152,51 @@
|
||||
<H3><A name="import_missing_module"></A><IMG alt="" src="icon.debugger.import"> Import Missing
|
||||
Module</H3>
|
||||
|
||||
<P>This action is offered to resolve a "missing module" console message. It is equivalent to <A
|
||||
href="#import_from_fs">Import From File System</A> on the missing module.</P>
|
||||
<P>This action is offered to resolve a "missing module" console message. Such a message is
|
||||
reported in the <A href="help/topics/DebuggerConsolePlugin/DebuggerConsolePlugin.html">Debug
|
||||
Console</A> when the cursor in the <A href=
|
||||
"help/topics/DebuggerListingPlugin/DebuggerListingPlugin.html">Dynamic Listing</A> cannot be
|
||||
synchronized to the static listing for lack of a module mapping. This action is equivalent to
|
||||
<A href="#import_from_fs">Import From File System</A> on the missing module.</P>
|
||||
|
||||
<H3><A name="map_missing_module"></A><IMG alt="" src="icon.debugger.map.modules"> Map Missing
|
||||
Module</H3>
|
||||
|
||||
<P>This action is offered to resolve a "missing module" console message. It is equivalent to <A
|
||||
href="#map_module_to">Map Module To</A> on the missing module.</P>
|
||||
<P>This action is offered to resolve a "missing module" console message. Such a message is
|
||||
reported in the <A href="help/topics/DebuggerConsolePlugin/DebuggerConsolePlugin.html">Debug
|
||||
Console</A> when the cursor in the <A href=
|
||||
"help/topics/DebuggerListingPlugin/DebuggerListingPlugin.html">Dynamic Listing</A> cannot be
|
||||
synchronized to the static listing for lack of a module mapping. This action is equivalent to
|
||||
<A href="#map_module_to">Map Module To</A> on the missing module.</P>
|
||||
|
||||
<H3><A name="map_missing_program_retry"></A><IMG alt="" src="icon.debugger.map.auto"> Retry Map
|
||||
Missing Program</H3>
|
||||
|
||||
<P>This action is offered to resolve a "missing program" console message. Such a message is
|
||||
reported in the <A href="help/topics/DebuggerConsolePlugin/DebuggerConsolePlugin.html">Debug
|
||||
Console</A> when the launcher fails to map the current program database to the launched trace.
|
||||
This action is equivalent to <A href="#map_modules">Map Modules</A>, but considering only the
|
||||
missing program and launched trace.</P>
|
||||
|
||||
<H3><A name="map_missing_program_current"></A><IMG alt="" src="icon.debugger.map.modules"> Map
|
||||
Missing Program to Current Module</H3>
|
||||
|
||||
<P>This action is offered to resolve a "missing program" console message. Such a message is
|
||||
reported in the <A href="help/topics/DebuggerConsolePlugin/DebuggerConsolePlugin.html">Debug
|
||||
Console</A> when the launcher fails to map the current program database to the launched trace.
|
||||
This action is only available when the current trace is the launched trace. It finds the module
|
||||
containing the cursor in the <A href=
|
||||
"help/topics/DebuggerListingPlugin/DebuggerListingPlugin.html">Dynamic Listing</A> and proposes
|
||||
to map it to the missing program.</P>
|
||||
|
||||
<H3><A name="map_missing_program_identically"></A><IMG alt="" src=
|
||||
"icon.debugger.map.identically"> Map Missing Program Identically</H3>
|
||||
|
||||
<P>This action is offered to resolve a "missing program" console message. Such a message is
|
||||
reported in the <A href="help/topics/DebuggerConsolePlugin/DebuggerConsolePlugin.html">Debug
|
||||
Console</A> when the launcher fails to map the current program database to the launched trace.
|
||||
This action is equivalent to <A href="#map_identically">Map Identically</A>, but for the
|
||||
missing program and launched trace.</P>
|
||||
|
||||
<H3><A name="show_sections_table"></A><IMG alt="" src="icon.debugger.modules.table.sections">
|
||||
Show Sections Table</H3>
|
||||
@@ -179,12 +216,15 @@
|
||||
Addresses</H3>
|
||||
|
||||
<P>This action is available when at least one module or section is selected. It selects all
|
||||
addresses in the dynamic listing contained by the selected modules or sections.</P>
|
||||
addresses in the <A href="help/topics/DebuggerListingPlugin/DebuggerListingPlugin.html">Dynamic
|
||||
Listing</A> contained by the selected modules or sections.</P>
|
||||
|
||||
<H3><A name="select_rows"></A><IMG alt="" src="icon.debugger.select.rows"> Select Rows</H3>
|
||||
|
||||
<P>This action is available when the dynamic listing's cursor is at a valid location. It
|
||||
selects the module and section, if applicable, containing that cursor. If the dynamic listing
|
||||
has a selection, it selects all modules and sections intersecting that selection.</P>
|
||||
<P>This action is available when the <A href=
|
||||
"help/topics/DebuggerListingPlugin/DebuggerListingPlugin.html">Dynamic Listing</A>'s cursor is
|
||||
at a valid location. It selects the module and section, if applicable, containing that cursor.
|
||||
If the dynamic listing has a selection, it selects all modules and sections intersecting that
|
||||
selection.</P>
|
||||
</BODY>
|
||||
</HTML>
|
||||
|
||||
+1
-1
@@ -56,7 +56,6 @@ import ghidra.app.plugin.core.debug.gui.DebuggerResources;
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources.FollowsCurrentThreadAction;
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources.OpenProgramAction;
|
||||
import ghidra.app.plugin.core.debug.gui.action.*;
|
||||
import ghidra.app.plugin.core.debug.gui.modules.DebuggerMissingModuleActionContext;
|
||||
import ghidra.app.plugin.core.debug.gui.thread.DebuggerTraceFileActionContext;
|
||||
import ghidra.app.plugin.core.debug.gui.trace.DebuggerTraceTabPanel;
|
||||
import ghidra.app.plugin.core.debug.utils.ProgramLocationUtils;
|
||||
@@ -74,6 +73,7 @@ import ghidra.debug.api.action.GoToInput;
|
||||
import ghidra.debug.api.action.LocationTrackingSpec;
|
||||
import ghidra.debug.api.control.ControlMode;
|
||||
import ghidra.debug.api.listing.MultiBlendedListingBackgroundColorModel;
|
||||
import ghidra.debug.api.modules.DebuggerMissingModuleActionContext;
|
||||
import ghidra.debug.api.modules.DebuggerStaticMappingChangeListener;
|
||||
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
|
||||
import ghidra.framework.model.DomainFile;
|
||||
|
||||
+5
@@ -20,6 +20,7 @@ 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.framework.options.SaveState;
|
||||
import ghidra.framework.plugintool.*;
|
||||
@@ -37,6 +38,7 @@ import ghidra.framework.plugintool.util.PluginStatus;
|
||||
ProgramLocationPluginEvent.class,
|
||||
ProgramClosedPluginEvent.class,
|
||||
TraceActivatedPluginEvent.class,
|
||||
TraceClosedPluginEvent.class,
|
||||
},
|
||||
servicesRequired = {
|
||||
DebuggerStaticMappingService.class,
|
||||
@@ -81,6 +83,9 @@ public class DebuggerModulesPlugin extends AbstractDebuggerPlugin {
|
||||
else if (event instanceof TraceActivatedPluginEvent ev) {
|
||||
provider.coordinatesActivated(ev.getActiveCoordinates());
|
||||
}
|
||||
else if (event instanceof TraceClosedPluginEvent ev) {
|
||||
provider.traceClosed(ev.getTrace());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+260
-22
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
package ghidra.app.plugin.core.debug.gui.modules;
|
||||
|
||||
import static ghidra.framework.main.DataTreeDialogType.*;
|
||||
import static ghidra.framework.main.DataTreeDialogType.OPEN;
|
||||
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.io.File;
|
||||
@@ -67,15 +67,29 @@ import ghidra.program.util.ProgramLocation;
|
||||
import ghidra.program.util.ProgramSelection;
|
||||
import ghidra.trace.model.*;
|
||||
import ghidra.trace.model.modules.*;
|
||||
import ghidra.trace.model.program.TraceProgramView;
|
||||
import ghidra.trace.util.TraceEvent;
|
||||
import ghidra.trace.util.TraceEvents;
|
||||
import ghidra.util.HelpLocation;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.*;
|
||||
|
||||
public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
||||
protected static final AutoConfigState.ClassHandler<DebuggerModulesProvider> CONFIG_STATE_HANDLER =
|
||||
AutoConfigState.wireHandler(DebuggerModulesProvider.class, MethodHandles.lookup());
|
||||
|
||||
protected static final String NO_MODULES_PROPOSAL_SEL = """
|
||||
Could not formulate a proposal for any selected module. \
|
||||
You may need to import and/or open the destination images first.\
|
||||
""";
|
||||
|
||||
protected static final String FMT_NO_MODULES_PROPOSAL_RETRY = """
|
||||
Could not formulate a proposal for program '%s' to trace '%s'. \
|
||||
The module may not be loaded yet, or the chosen image could be wrong.\
|
||||
""";
|
||||
|
||||
protected static final String FMT_NO_MODULES_PROPOSAL_CURRENT = """
|
||||
Could not formulate a proposal from module '%s' to program '%s'.\
|
||||
""";
|
||||
|
||||
protected static boolean sameCoordinates(DebuggerCoordinates a, DebuggerCoordinates b) {
|
||||
if (!Objects.equals(a.getTrace(), b.getTrace())) {
|
||||
return false;
|
||||
@@ -253,6 +267,57 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
interface MapMissingProgramRetryAction {
|
||||
String NAME = "Retry Map Missing Program";
|
||||
String DESCRIPTION = "Retry mapping the missing program by finding its module";
|
||||
Icon ICON = DebuggerResources.ICON_MAP_AUTO;
|
||||
String HELP_ANCHOR = "map_missing_program_retry";
|
||||
|
||||
static ActionBuilder builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(NAME, ownerName)
|
||||
.description(DESCRIPTION)
|
||||
.toolBarIcon(ICON)
|
||||
.popupMenuIcon(ICON)
|
||||
.popupMenuPath(NAME)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
||||
|
||||
interface MapMissingProgramToCurrentAction {
|
||||
String NAME = "Map Missing Program to Current Module";
|
||||
String DESCRIPTION = "Map the missing program to the current module";
|
||||
Icon ICON = DebuggerResources.ICON_MAP_MODULES;
|
||||
String HELP_ANCHOR = "map_missing_program_current";
|
||||
|
||||
static ActionBuilder builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(NAME, ownerName)
|
||||
.description(DESCRIPTION)
|
||||
.toolBarIcon(ICON)
|
||||
.popupMenuIcon(ICON)
|
||||
.popupMenuPath(NAME)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
||||
|
||||
interface MapMissingProgramIdenticallyAction {
|
||||
String NAME = "Map Missing Program Identically";
|
||||
String DESCRIPTION = "Map the missing program to its trace identically";
|
||||
Icon ICON = DebuggerResources.ICON_MAP_IDENTICALLY;
|
||||
String HELP_ANCHOR = "map_missing_program_identically";
|
||||
|
||||
static ActionBuilder builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(NAME, ownerName)
|
||||
.description(DESCRIPTION)
|
||||
.toolBarIcon(ICON)
|
||||
.popupMenuIcon(ICON)
|
||||
.popupMenuPath(NAME)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
||||
|
||||
interface ShowSectionsTableAction {
|
||||
String NAME = "Show Sections Table";
|
||||
Icon ICON = new GIcon("icon.debugger.modules.table.sections");
|
||||
@@ -404,9 +469,17 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
protected class ForCleanupMappingChangeListener
|
||||
implements DebuggerStaticMappingChangeListener {
|
||||
@Override
|
||||
public void mappingsChanged(Set<Trace> affectedTraces, Set<Program> affectedPrograms) {
|
||||
Swing.runIfSwingOrRunLater(() -> cleanMissingProgramMessages(null, null));
|
||||
}
|
||||
}
|
||||
|
||||
final DebuggerModulesPlugin plugin;
|
||||
|
||||
@AutoServiceConsumed
|
||||
//@AutoServiceConsumed via method
|
||||
private DebuggerStaticMappingService staticMappingService;
|
||||
@AutoServiceConsumed
|
||||
private DebuggerTraceManagerService traceManager;
|
||||
@@ -467,6 +540,13 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
||||
DockingAction actionImportMissingModule;
|
||||
DockingAction actionMapMissingModule;
|
||||
|
||||
DockingAction actionMapMissingProgramRetry;
|
||||
DockingAction actionMapMissingProgramToCurrent;
|
||||
DockingAction actionMapMissingProgramIdentically;
|
||||
|
||||
protected final ForCleanupMappingChangeListener mappingChangeListener =
|
||||
new ForCleanupMappingChangeListener();
|
||||
|
||||
SelectAddressesAction actionSelectAddresses;
|
||||
ImportFromFileSystemAction actionImportFromFileSystem;
|
||||
ToggleDockingAction actionShowSectionsTable;
|
||||
@@ -507,26 +587,37 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
||||
importerService.importFile(root, file);
|
||||
}
|
||||
|
||||
void addResolutionActionMaybe(DebuggerConsoleService consoleService, DockingActionIf action) {
|
||||
if (action != null) {
|
||||
consoleService.addResolutionAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
void removeResolutionActionMaybe(DebuggerConsoleService consoleService,
|
||||
DockingActionIf action) {
|
||||
if (action != null) {
|
||||
consoleService.removeResolutionAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
@AutoServiceConsumed
|
||||
private void setConsoleService(DebuggerConsoleService consoleService) {
|
||||
if (consoleService != null) {
|
||||
if (actionImportMissingModule != null) {
|
||||
consoleService.addResolutionAction(actionImportMissingModule);
|
||||
}
|
||||
if (actionMapMissingModule != null) {
|
||||
consoleService.addResolutionAction(actionMapMissingModule);
|
||||
}
|
||||
addResolutionActionMaybe(consoleService, actionImportMissingModule);
|
||||
addResolutionActionMaybe(consoleService, actionMapMissingModule);
|
||||
addResolutionActionMaybe(consoleService, actionMapMissingProgramRetry);
|
||||
addResolutionActionMaybe(consoleService, actionMapMissingProgramToCurrent);
|
||||
addResolutionActionMaybe(consoleService, actionMapMissingProgramIdentically);
|
||||
}
|
||||
}
|
||||
|
||||
protected void dispose() {
|
||||
if (consoleService != null) {
|
||||
if (actionImportMissingModule != null) {
|
||||
consoleService.removeResolutionAction(actionImportMissingModule);
|
||||
}
|
||||
if (actionMapMissingModule != null) {
|
||||
consoleService.removeResolutionAction(actionMapMissingModule);
|
||||
}
|
||||
removeResolutionActionMaybe(consoleService, actionImportMissingModule);
|
||||
removeResolutionActionMaybe(consoleService, actionMapMissingModule);
|
||||
removeResolutionActionMaybe(consoleService, actionMapMissingProgramRetry);
|
||||
removeResolutionActionMaybe(consoleService, actionMapMissingProgramToCurrent);
|
||||
removeResolutionActionMaybe(consoleService, actionMapMissingProgramIdentically);
|
||||
}
|
||||
|
||||
blockChooserDialog.dispose();
|
||||
@@ -637,6 +728,20 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
||||
.onAction(this::activatedMapMissingModule)
|
||||
.build();
|
||||
|
||||
actionMapMissingProgramRetry = MapMissingProgramRetryAction.builder(plugin)
|
||||
.withContext(DebuggerMissingProgramActionContext.class)
|
||||
.onAction(this::activatedMapMissingProgramRetry)
|
||||
.build();
|
||||
actionMapMissingProgramToCurrent = MapMissingProgramToCurrentAction.builder(plugin)
|
||||
.withContext(DebuggerMissingProgramActionContext.class)
|
||||
.enabledWhen(this::isEnabledMapMissingProgramToCurrent)
|
||||
.onAction(this::activatedMapMissingProgramToCurrent)
|
||||
.build();
|
||||
actionMapMissingProgramIdentically = MapMissingProgramIdenticallyAction.builder(plugin)
|
||||
.withContext(DebuggerMissingProgramActionContext.class)
|
||||
.onAction(this::activatedMapMissingProgramIdentically)
|
||||
.build();
|
||||
|
||||
actionSelectAddresses = new SelectAddressesAction();
|
||||
actionImportFromFileSystem = new ImportFromFileSystemAction();
|
||||
actionShowSectionsTable = ShowSectionsTableAction.builder(plugin)
|
||||
@@ -799,6 +904,78 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
||||
mapModuleTo(context.getModule());
|
||||
}
|
||||
|
||||
private void activatedMapMissingProgramRetry(DebuggerMissingProgramActionContext context) {
|
||||
if (staticMappingService == null) {
|
||||
return;
|
||||
}
|
||||
Program program = context.getProgram();
|
||||
Trace trace = context.getTrace();
|
||||
Map<TraceModule, ModuleMapProposal> map = staticMappingService.proposeModuleMaps(
|
||||
trace.getModuleManager().getAllModules(), List.of(program));
|
||||
Collection<ModuleMapEntry> proposal = MapProposal.flatten(map.values());
|
||||
promptModuleProposal(proposal, FMT_NO_MODULES_PROPOSAL_RETRY.formatted(
|
||||
trace.getDomainFile().getName(), program.getDomainFile().getName()));
|
||||
}
|
||||
|
||||
private boolean isEnabledMapMissingProgramToCurrent(
|
||||
DebuggerMissingProgramActionContext context) {
|
||||
if (staticMappingService == null || traceManager == null || listingService == null) {
|
||||
return false;
|
||||
}
|
||||
ProgramLocation loc = listingService.getCurrentLocation();
|
||||
if (loc == null) {
|
||||
return false;
|
||||
}
|
||||
if (!(loc.getProgram() instanceof TraceProgramView view)) {
|
||||
return false;
|
||||
}
|
||||
Trace trace = context.getTrace();
|
||||
if (view.getTrace() != trace) {
|
||||
return false;
|
||||
}
|
||||
|
||||
long snap = traceManager.getCurrentFor(trace).getSnap();
|
||||
Address address = loc.getAddress();
|
||||
return !trace.getModuleManager().getModulesAt(snap, address).isEmpty();
|
||||
}
|
||||
|
||||
private void activatedMapMissingProgramToCurrent(DebuggerMissingProgramActionContext context) {
|
||||
if (staticMappingService == null || traceManager == null || listingService == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Trace trace = context.getTrace();
|
||||
long snap = traceManager.getCurrentFor(trace).getSnap();
|
||||
Address address = listingService.getCurrentLocation().getAddress();
|
||||
|
||||
TraceModule module = trace.getModuleManager().getModulesAt(snap, address).iterator().next();
|
||||
|
||||
Program program = context.getProgram();
|
||||
ModuleMapProposal proposal =
|
||||
staticMappingService.proposeModuleMap(module, program);
|
||||
Map<TraceModule, ModuleMapEntry> map = proposal.computeMap();
|
||||
promptModuleProposal(map.values(), FMT_NO_MODULES_PROPOSAL_CURRENT.formatted(
|
||||
module.getName(), program.getDomainFile().getName()));
|
||||
}
|
||||
|
||||
private void activatedMapMissingProgramIdentically(
|
||||
DebuggerMissingProgramActionContext context) {
|
||||
if (staticMappingService == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Trace trace = context.getTrace();
|
||||
long snap = traceManager == null ? 0 : traceManager.getCurrentFor(trace).getSnap();
|
||||
|
||||
try {
|
||||
staticMappingService.addIdentityMapping(trace, context.getProgram(),
|
||||
Lifespan.nowOn(snap), true);
|
||||
}
|
||||
catch (TraceConflictedMappingException e) {
|
||||
Msg.showError(this, null, "Map Identically", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void toggledShowSectionsTable(ActionContext ignored) {
|
||||
setShowSectionsTable(actionShowSectionsTable.isSelected());
|
||||
}
|
||||
@@ -903,11 +1080,9 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
protected void promptModuleProposal(Collection<ModuleMapEntry> proposal) {
|
||||
protected void promptModuleProposal(Collection<ModuleMapEntry> proposal, String emptyMsg) {
|
||||
if (proposal.isEmpty()) {
|
||||
Msg.showInfo(this, getComponent(), "Map Modules",
|
||||
"Could not formulate a proposal for any selected module." +
|
||||
" You may need to import and/or open the destination images first.");
|
||||
Msg.showInfo(this, getComponent(), "Map Modules", emptyMsg);
|
||||
return;
|
||||
}
|
||||
Collection<ModuleMapEntry> adjusted =
|
||||
@@ -926,7 +1101,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
||||
Map<TraceModule, ModuleMapProposal> map = staticMappingService.proposeModuleMaps(modules,
|
||||
List.of(programManager.getAllOpenPrograms()));
|
||||
Collection<ModuleMapEntry> proposal = MapProposal.flatten(map.values());
|
||||
promptModuleProposal(proposal);
|
||||
promptModuleProposal(proposal, NO_MODULES_PROPOSAL_SEL);
|
||||
}
|
||||
|
||||
protected void mapModuleTo(TraceModule module) {
|
||||
@@ -939,7 +1114,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
||||
}
|
||||
ModuleMapProposal proposal = staticMappingService.proposeModuleMap(module, program);
|
||||
Map<TraceModule, ModuleMapEntry> map = proposal.computeMap();
|
||||
promptModuleProposal(map.values());
|
||||
promptModuleProposal(map.values(), NO_MODULES_PROPOSAL_SEL);
|
||||
}
|
||||
|
||||
protected void promptSectionProposal(Collection<SectionMapEntry> proposal) {
|
||||
@@ -1090,6 +1265,11 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
||||
if (currentProgram == program) {
|
||||
currentProgram = null;
|
||||
}
|
||||
cleanMissingProgramMessages(null, program);
|
||||
}
|
||||
|
||||
public void traceClosed(Trace trace) {
|
||||
cleanMissingProgramMessages(trace, null);
|
||||
}
|
||||
|
||||
protected void addNewTraceListener() {
|
||||
@@ -1232,4 +1412,62 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
||||
doSetFilterSectionsByModules(filterSectionsByModules);
|
||||
doSetShowSectionsTable(showSectionsTable);
|
||||
}
|
||||
|
||||
protected boolean shouldKeepMessage(DebuggerMissingProgramActionContext ctx, Trace closedTrace,
|
||||
Program closedProgram) {
|
||||
Trace trace = ctx.getTrace();
|
||||
if (trace == closedTrace) {
|
||||
return false;
|
||||
}
|
||||
if (!traceManager.getOpenTraces().contains(trace)) {
|
||||
return false;
|
||||
}
|
||||
Program program = ctx.getProgram();
|
||||
if (program == closedProgram) {
|
||||
return false;
|
||||
}
|
||||
if (programManager != null &&
|
||||
!Arrays.asList(programManager.getAllOpenPrograms()).contains(program)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only do mapping probe on mapping changed events
|
||||
if (closedTrace != null || closedProgram != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
TraceProgramView view = traceManager.getCurrentFor(trace).getView();
|
||||
Address probe = ctx.getMappingProbeAddress();
|
||||
ProgramLocation dyn = staticMappingService.getDynamicLocationFromStatic(view,
|
||||
new ProgramLocation(program, probe));
|
||||
if (dyn != null) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void cleanMissingProgramMessages(Trace closedTrace, Program closedProgram) {
|
||||
if (traceManager == null) {
|
||||
return;
|
||||
}
|
||||
for (ActionContext ctx : consoleService.getActionContexts()) {
|
||||
if (!(ctx instanceof DebuggerMissingProgramActionContext mpCtx)) {
|
||||
continue;
|
||||
}
|
||||
if (!shouldKeepMessage(mpCtx, closedTrace, closedProgram)) {
|
||||
consoleService.removeFromLog(mpCtx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AutoServiceConsumed
|
||||
private void setStaticMappingService(DebuggerStaticMappingService staticMappingService) {
|
||||
if (this.staticMappingService != null) {
|
||||
this.staticMappingService.removeChangeListener(mappingChangeListener);
|
||||
}
|
||||
this.staticMappingService = staticMappingService;
|
||||
if (this.staticMappingService != null) {
|
||||
this.staticMappingService.addChangeListener(mappingChangeListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -47,13 +47,13 @@ import ghidra.app.plugin.core.debug.gui.action.*;
|
||||
import ghidra.app.plugin.core.debug.gui.console.DebuggerConsolePlugin;
|
||||
import ghidra.app.plugin.core.debug.gui.console.DebuggerConsoleProvider.BoundAction;
|
||||
import ghidra.app.plugin.core.debug.gui.console.DebuggerConsoleProvider.LogRow;
|
||||
import ghidra.app.plugin.core.debug.gui.modules.DebuggerMissingModuleActionContext;
|
||||
import ghidra.app.plugin.core.debug.service.modules.DebuggerStaticMappingUtils;
|
||||
import ghidra.app.services.DebuggerStaticMappingService;
|
||||
import ghidra.app.services.ProgramManager;
|
||||
import ghidra.app.util.viewer.listingpanel.ListingPanel;
|
||||
import ghidra.async.SwingExecutorService;
|
||||
import ghidra.debug.api.model.TraceRecorder;
|
||||
import ghidra.debug.api.modules.DebuggerMissingModuleActionContext;
|
||||
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
|
||||
import ghidra.framework.model.*;
|
||||
import ghidra.plugin.importer.ImporterPlugin;
|
||||
|
||||
+5
@@ -139,4 +139,9 @@ public class DefaultTerminal implements Terminal {
|
||||
public void setTerminateAction(Runnable action) {
|
||||
Swing.runIfSwingOrRunLater(() -> provider.setTerminateAction(action));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toFront() {
|
||||
provider.toFront();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -802,7 +802,8 @@ public class VtBuffer {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
for (int i = Math.max(sbSize, startRow); i <= endRow; i++) {
|
||||
int lnSize = lines.size();
|
||||
for (int i = Math.max(sbSize, startRow); i <= Math.min(endRow, sbSize + lnSize - 1); i++) {
|
||||
VtLine line = lines.get(i - sbSize);
|
||||
gatherLineText(buf, startRow, startCol, endRow, endCol, i, line, lineSep);
|
||||
}
|
||||
|
||||
@@ -55,6 +55,8 @@ public interface Terminal extends AutoCloseable {
|
||||
|
||||
/**
|
||||
* @see #injectDisplayOutput(ByteBuffer)
|
||||
*
|
||||
* @param arr the array of bytes to inject
|
||||
*/
|
||||
default void injectDisplayOutput(byte[] arr) {
|
||||
injectDisplayOutput(ByteBuffer.wrap(arr));
|
||||
@@ -87,6 +89,9 @@ public interface Terminal extends AutoCloseable {
|
||||
|
||||
/**
|
||||
* @see #setFixedSize(short, short)
|
||||
*
|
||||
* @param cols the number of columns
|
||||
* @param rows the number of rows
|
||||
*/
|
||||
default void setFixedSize(int cols, int rows) {
|
||||
setFixedSize((short) cols, (short) rows);
|
||||
@@ -102,6 +107,8 @@ public interface Terminal extends AutoCloseable {
|
||||
*
|
||||
* <p>
|
||||
* This only affects the primary buffer. The alternate buffer has no scroll-back.
|
||||
*
|
||||
* @param rows the number of scroll-back rows
|
||||
*/
|
||||
void setMaxScrollBackRows(int rows);
|
||||
|
||||
@@ -210,4 +217,9 @@ public interface Terminal extends AutoCloseable {
|
||||
* @return true for terminated, false for active
|
||||
*/
|
||||
boolean isTerminated();
|
||||
|
||||
/**
|
||||
* Bring the terminal to the front of the UI
|
||||
*/
|
||||
void toFront();
|
||||
}
|
||||
|
||||
+17
@@ -482,6 +482,23 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("resource")
|
||||
public void testGetFullText() throws Exception {
|
||||
terminalService = addPlugin(tool, TerminalPlugin.class);
|
||||
|
||||
try (DefaultTerminal term = (DefaultTerminal) terminalService
|
||||
.createNullTerminal(Charset.forName("UTF-8"), buf -> {
|
||||
})) {
|
||||
term.setFixedSize(80, 25);
|
||||
term.injectDisplayOutput(TEST_CONTENTS);
|
||||
|
||||
assertEquals("""
|
||||
term Term
|
||||
noterm""", term.getFullText().trim());
|
||||
}
|
||||
}
|
||||
|
||||
protected String csi(char f, int... params) {
|
||||
return "\033[" +
|
||||
IntStream.of(params).mapToObj(Integer::toString).collect(Collectors.joining(";")) + f;
|
||||
|
||||
Reference in New Issue
Block a user