GP-6505: Fix TraceRmiPythonClientTest on Windows

This commit is contained in:
Dan
2026-03-25 18:25:40 +00:00
parent cccc5103c1
commit d143ad03cc
4 changed files with 162 additions and 186 deletions

View File

@@ -94,6 +94,7 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler {
// Flags for what's been enabled
protected boolean showCursor;
protected boolean bracketedPaste;
protected boolean win32InputMode; // not implemented
protected boolean reportMousePress;
protected boolean reportMouseRelease;
protected boolean reportFocus;
@@ -139,6 +140,7 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler {
buffer = bufPrimary;
bracketedPaste = false;
win32InputMode = false;
reportMousePress = false;
reportMouseRelease = false;
reportFocus = false;
@@ -497,6 +499,11 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler {
public void handleBracketedPasteMode(boolean en) {
this.bracketedPaste = en;
}
@Override
public void handleWin32InputMode(boolean en) {
this.win32InputMode = en;
}
@Override
public void handleSaveCursorPos() {

View File

@@ -28,10 +28,8 @@ import ghidra.util.Msg;
/**
* The handler of parsed ANSI VT control sequences
*
* <p>
* Here are some of the resources where I found useful documentation:
*
* <ul>
* <li><a href="https://invisible-island.net/xterm/ctlseqs/ctlseqs.html">XTerm Control
* Sequences</a></li>
@@ -39,13 +37,11 @@ import ghidra.util.Msg;
* Terminal Control Escape Sequences</a></li>
* <li><a href="https://en.wikipedia.org/wiki/ANSI_escape_code">Wikipedia: ANSI escape code</a></li>
* </ul>
*
* <p>
* They were incredibly useful, even when experimentation was required to fill in details, because
* they at least described the sort of behavior I should be looking for. Throughout the referenced
* documents and within this documentation, the following abbreviations are used for escape
* sequences:
*
* <table>
* <tr>
* <th>Abbreviation</th>
@@ -78,13 +74,11 @@ import ghidra.util.Msg;
* <td>{@code "\007"}</td>
* </tr>
* </table>
*
* <p>
* The separation between the parser and the handler deals in state management. The parser manages
* state only of the control sequence parser itself, i.e., the current node in the token parsing
* state machine. The state of the terminal, e.g., the current attributes, cursor position, etc.,
* are managed by the handler and its delegates.
*
* <p>
* For example, the Cursor Position sequence is documented as:
* <p>
@@ -97,7 +91,6 @@ import ghidra.util.Msg;
* It will thus invoke the abstract {@link #handleMoveCursor(int, int)} method passing 12 and 39.
* Note that 1 is subtracted from both parameters, because ANSI specifies 1-up indexing while Java
* lends itself to 0-up indexing.
*
* <p>
* The XTerm documentation, which is arguably the most thorough, presents the CSI commands
* alphabetically by the final byte, in ASCII order. For sanity and consistency, we adopt the same
@@ -134,10 +127,10 @@ public interface VtHandler {
public static final byte[] Q1048 = ascii("?1048");
public static final byte[] Q1049 = ascii("?1049");
public static final byte[] Q2004 = ascii("?2004");
public static final byte[] Q9001 = ascii("?9001");
/**
* An ANSI color specification
*
* <p>
* We avoid going straight to AWT colors, 1) Because it provides better separation between the
* terminal logic and the rendering framework, and 2) Because some specifications, e.g., default
@@ -149,7 +142,6 @@ public interface VtHandler {
/**
* A singleton representing the default color
*
* <p>
* The actual color selected will depend on context and use. Most notably, the default color
* used for foreground should greatly contrast the default color used for the background.
@@ -160,7 +152,6 @@ public interface VtHandler {
/**
* One of the eight standard ANSI colors
*
* <p>
* The actual color may be modified by other SGR attributes, notably {@link Intensity}. For
* colors that are described by hue, some thought should be given to how the standard and
@@ -219,7 +210,6 @@ public interface VtHandler {
/**
* Get the standard color for the given numerical code
*
* <p>
* For example, the sequence {@code CSI [ 34 m} would use code 4 (blue).
*
@@ -233,7 +223,6 @@ public interface VtHandler {
/**
* One of the eight ANSI intense colors
*
* <p>
* Note that intense colors may also be specified using the standard color with the
* {@link Intensity#BOLD} attribute, depending on the command sequence.
@@ -279,7 +268,6 @@ public interface VtHandler {
/**
* Get the intense color for the given numerical code
*
* <p>
* For example, the sequence {@code CSI [ 94 m} would use code 4 (blue).
*
@@ -335,7 +323,6 @@ public interface VtHandler {
/**
* Get the dim color for the given numerical code
*
* <p>
* For example, the sequence {@code CSI [ 34 m} would use code 4 (blue).
*
@@ -349,7 +336,6 @@ public interface VtHandler {
/**
* For 8-bit colors, one of the 216 colors from the RGB cube
*
* <p>
* The r, g, and b fields give the "step" number from 0 to 5, dimmest to brightest.
*/
@@ -357,7 +343,6 @@ public interface VtHandler {
/**
* For 8-bit colors, one of the 24 grays
*
* <p>
* The v field is a value from 0 to 23, 0 being the dimmest, but not true black, and 23 being
* the brightest, but not true white.
@@ -366,7 +351,6 @@ public interface VtHandler {
/**
* A 24-bit color
*
* <p>
* The r, g, and b fields are values from 0 to 255 dimmest to brightest.
*/
@@ -374,7 +358,6 @@ public interface VtHandler {
/**
* Modifies the intensity of the character either by color or by font weight.
*
* <p>
* The renderer may choose a combination of strategies. For example, {@link #NORMAL} may be
* rendered using the standard color and bold type. Then {@link #BOLD} would use the intense
@@ -434,7 +417,6 @@ public interface VtHandler {
/**
* Causes text to blink
*
* <p>
* If implemented, renderers should take care not to irritate the user. One option is to make
* {@link #FAST} actually slow, and {@link #SLOW} even slower. Another option is to only blink
@@ -567,7 +549,6 @@ public interface VtHandler {
/**
* For cursor and keypad, specifies normal or application mode
*
* <p>
* This affects the codes sent by the terminal.
*/
@@ -628,7 +609,6 @@ public interface VtHandler {
/**
* Handle normal character output, i.e., place the character on the display
*
* <p>
* This excludes control sequences and control characters, e.g., tab, line feed. While we've not
* tested, in theory, this can instead buffer the byte for decoding from UTF-8. Still, the
@@ -641,7 +621,6 @@ public interface VtHandler {
/**
* Handle a character not part of an escape sequence.
*
* <p>
* This may include control characters, which are displatched appropriately by this method.
* Additionally, this handles any exception thrown by {@link #handleChar(byte)}.
@@ -683,7 +662,6 @@ public interface VtHandler {
/**
* Parse a sequence of integers in the form {@code <em>n</em> ; <em>m</em> ;} ....
*
* <p>
* This is designed to replace the {@link String#split(String)} and
* {@link Integer#parseInt(String)} pattern, which should avoid some unnecessary object
@@ -799,6 +777,9 @@ public interface VtHandler {
else if (bufEq(csiParam, Q2004)) {
handleBracketedPasteMode(en);
}
else if (bufEq(csiParam, Q9001)) {
handleWin32InputMode(en);
}
else {
throw new UnknownCsiException();
}
@@ -1189,7 +1170,6 @@ public interface VtHandler {
/**
* Decode the 8-bit ANSI color.
*
* <p>
* Colors 0-15 are the standard + high-intensity. Colors 16-231 come from a 6x6x6 RGB cube.
* Finally, colors 232-255 are 24 steps of gray scale.
@@ -1413,7 +1393,6 @@ public interface VtHandler {
/**
* Handle toggling of reverse video
*
* <p>
* This can be a bit confusing with default colors. In general, this means swapping the
* foreground and background color specifications (not inverting the colors or mirroring or some
@@ -1449,7 +1428,6 @@ public interface VtHandler {
/**
* Handle toggling insert mode
*
* <p>
* In insert mode, characters at and to the right of the cursor are shifted right to make room
* for each new character. In replace mode (default), the character at the cursor is replaced
@@ -1482,7 +1460,6 @@ public interface VtHandler {
/**
* Toggle blinking of the cursor
*
* <p>
* Renderers should take care not to irritate the user. Some possibilities are to blink slowly,
* blink only for a short period of time after it moves, and/or blink only when the terminal has
@@ -1523,7 +1500,6 @@ public interface VtHandler {
/**
* Switch to and from the alternate screen buffer, optionally clearing it
*
* <p>
* This will never clear the normal buffer. If the buffer does not change as a result of this
* call, then the alternate buffer is not cleared, even if clearAlt is specified.
@@ -1545,6 +1521,20 @@ public interface VtHandler {
*/
void handleBracketedPasteMode(boolean en);
/**
* Toggle Win32 input mode
*
* <p>
* See the Windows Terminal specification:
* https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md.
* It should be safe to ignore this, but could provide us options if we'd like to forward more
* detailed keyboard events to a Windows Console application than is permitted with standard VT
* sequences.
*
* @param en true to enable Win32 input mode
*/
void handleWin32InputMode(boolean en);
/**
* Handle a request to save the cursor position
*/
@@ -1573,7 +1563,6 @@ public interface VtHandler {
/**
* Handle an absolute cursor row movement command
*
* <p>
* The column should remain the same, i.e., do <em>not</em> reset the column to 0.
*
@@ -1595,7 +1584,6 @@ public interface VtHandler {
/**
* Handle a request to save the terminal window's icon title
*
* <p>
* "Icon titles" are a concept from the X Windows system. Do the closest equivalent, if anything
* applies at all. The current title is pushed to a stack of limited size.
@@ -1604,7 +1592,6 @@ public interface VtHandler {
/**
* Handle a request to save the terminal window's title
*
* <p>
* Window titles are fairly applicable to all desktop windowing systems. The current title is
* pushed to a stack of limited size.
@@ -1613,7 +1600,6 @@ public interface VtHandler {
/**
* Handle a request to restore the terminal window's icon title
*
* <p>
* The title is set to the one popped from the stack of saved window icon titles.
*
@@ -1623,7 +1609,6 @@ public interface VtHandler {
/**
* Handle a request to restore the terminal window's title
*
* <p>
* The title is set to the one popped from the stack of saved window titles.
*
@@ -1647,7 +1632,6 @@ public interface VtHandler {
/**
* Insert n lines at and below the cursor
*
* <p>
* Lines within the viewport are shifted down or deleted to make room for the new lines.
*
@@ -1657,7 +1641,6 @@ public interface VtHandler {
/**
* Delete n lines at and below the cursor
*
* <p>
* Lines within the viewport are shifted up, and new lines inserted at the bottom.
*
@@ -1702,7 +1685,6 @@ public interface VtHandler {
/**
* Set the range of rows (viewport) involved in scrolling.
*
* <p>
* This applies not only to {@link #handleScrollUp()} and {@link #handleScrollDown()}, but also
* to when the cursor moves far enough down that the display must scroll. Normally, start is 0
@@ -1721,7 +1703,6 @@ public interface VtHandler {
/**
* Scroll the display n lines down, considering only those lines in the scrolling range.
*
* <p>
* To be unambiguous, this of movement of the viewport. The viewport scrolls down, so the lines
* themselves scroll up. The default range is the whole display. The cursor is not moved.
@@ -1743,7 +1724,6 @@ public interface VtHandler {
/**
* Scroll the lines n slots down, considering only those lines in the scrolling range.
*
* <p>
* This is equivalent to scrolling the <em>viewport</em> n lines <em>up</em>. This method exists
* in attempt to reflect "up" and "down" correctly in the documentation. Unfortunately, the
@@ -1759,7 +1739,6 @@ public interface VtHandler {
/**
* Scroll the lines n slots up, considering only those lines in the scrolling range.
*
* <p>
* The is equivalent to scrolling the <em>viewport</em> n lines <em>down</em>. This method
* exists in attempt to reflect "up" and "down" correctly in the documentation. Unfortunately,
@@ -1784,7 +1763,6 @@ public interface VtHandler {
/**
* Handle a request to fully reset the terminal
*
* <p>
* All buffers should be cleared and all state variables, positions, attributes, etc., should be
* reset to their defaults.

View File

@@ -19,7 +19,6 @@ import java.io.*;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Stream;
// TODO: I shouldn't have to do any of this.
@@ -118,34 +117,17 @@ public class AnsiBufferedInputStream extends InputStream {
return -1;
}
byte c = (byte) ci;
//printDebugChar(c);
// printDebugChar(c);
switch (mode) {
case CHARS:
processChars(c);
break;
case ESC:
processEsc(c);
break;
case CSI:
processCsi(c);
break;
case CSI_p:
processCsiParamOrCommand(c);
break;
case CSI_Q:
processCsiQ(c);
break;
case OSC:
processOsc(c);
break;
case WINDOW_TITLE:
processWindowTitle(c);
break;
case WINDOW_TITLE_ESC:
processWindowTitleEsc(c);
break;
default:
throw new AssertionError();
case CHARS -> processChars(c);
case ESC -> processEsc(c);
case CSI -> processCsi(c);
case CSI_p -> processCsiParamOrCommand(c);
case CSI_Q -> processCsiQ(c);
case OSC -> processOsc(c);
case WINDOW_TITLE -> processWindowTitle(c);
case WINDOW_TITLE_ESC -> processWindowTitleEsc(c);
default -> throw new AssertionError();
}
countIn++;
return c;
@@ -187,180 +169,151 @@ public class AnsiBufferedInputStream extends InputStream {
protected void processChars(byte c) {
switch (c) {
case 0x08:
default -> appendChar(c);
case '\b' -> {
if (lineBuf.get(lineBuf.position() - 1) == ' ') {
lineBuf.position(lineBuf.position() - 1);
}
break;
case '\n':
//appendChar(c);
bakeLine();
break;
case 0x1b:
mode = Mode.ESC;
break;
default:
appendChar(c);
break;
}
case '\n' -> bakeLine();
case '\r' -> lineBuf.position(0);
case 0x1b -> mode = Mode.ESC;
}
}
protected void processEsc(byte c) {
switch (c) {
case '[':
mode = Mode.CSI;
break;
case ']':
mode = Mode.OSC;
break;
default:
throw new AssertionError("Saw 'ESC " + c + "' at " + countIn);
case '[' -> mode = Mode.CSI;
case ']' -> mode = Mode.OSC;
default -> throw new AssertionError("Saw 'ESC " + c + "' at " + countIn);
}
}
protected void processCsi(byte c) {
switch (c) {
default:
processCsiParamOrCommand(c);
break;
case '?':
mode = Mode.CSI_Q;
break;
default -> processCsiParamOrCommand(c);
case '?' -> mode = Mode.CSI_Q;
}
}
protected void processCsiParamOrCommand(byte c) {
switch (c) {
default:
escBuf.put(c);
break;
case 'A':
default -> escBuf.put(c);
case 'A' -> {
execCursorUp();
mode = Mode.CHARS;
break;
case 'B':
}
case 'B' -> {
execCursorDown();
mode = Mode.CHARS;
break;
case 'C':
}
case 'C' -> {
execCursorForward();
mode = Mode.CHARS;
break;
case 'D':
}
case 'D' -> {
execCursorBackward();
mode = Mode.CHARS;
break;
case 'G':
}
case 'G' -> {
execCursorCharAbsolute();
mode = Mode.CHARS;
break;
case 'H':
}
case 'H' -> {
execCursorPosition();
mode = Mode.CHARS;
break;
case 'J':
}
case 'J' -> {
execEraseInDisplay();
mode = Mode.CHARS;
break;
case 'K':
}
case 'K' -> {
execEraseInLine();
mode = Mode.CHARS;
break;
case 'X':
}
case 'X' -> {
execEraseCharacter();
mode = Mode.CHARS;
break;
case 'm':
}
case 'm' -> {
execSetGraphicsRendition();
mode = Mode.CHARS;
break;
case 'h':
}
case 'h' -> {
execPrivateSequence(true);
mode = Mode.CHARS;
break;
case 'l':
}
case 'l' -> {
execPrivateSequence(false);
mode = Mode.CHARS;
break;
}
}
}
public static final String PRIV_12 = "12";
public static final String PRIV_25 = "25";
public static final String PRIV_1004 = "1004";
public static final String PRIV_2004 = "2004";
public static final String PRIV_9001 = "9001";
protected void processCsiQ(byte c) {
String buf;
switch (c) {
default:
escBuf.put(c);
break;
case 'h':
buf = readAndClearEscBuf();
if ("12".equals(buf)) {
execTextCursorEnableBlinking();
escBuf.clear();
mode = Mode.CHARS;
default -> escBuf.put(c);
case 'h' -> {
switch (readAndClearEscBuf()) {
case PRIV_12 -> execTextCursorEnableBlinking();
case PRIV_25 -> execTextCursorEnableModeShow();
case PRIV_1004 -> execEnableFocusReport();
case PRIV_2004 -> execEnableBracketedPasteMode();
case PRIV_9001 -> execEnableWin32InputMode();
case String buf -> throw new AssertionError("Got CsiQ(h): %s".formatted(buf));
}
else if ("25".equals(buf)) {
execTextCursorEnableModeShow();
escBuf.clear();
mode = Mode.CHARS;
mode = Mode.CHARS;
}
case 'l' -> {
switch (readAndClearEscBuf()) {
case PRIV_12 -> execTextCursorDisableBlinking();
case PRIV_25 -> execTextCursorDisableModeShow();
case PRIV_1004 -> execDisableFocusReport();
case PRIV_2004 -> execDisableBracketedPasteMode();
case PRIV_9001 -> execDisableWin32InputMode();
case String buf -> throw new AssertionError("Got CsiQ(l): %s".formatted(buf));
}
else {
throw new AssertionError();
}
break;
case 'l':
buf = readAndClearEscBuf();
if ("12".equals(buf)) {
execTextCursorDisableBlinking();
escBuf.clear();
mode = Mode.CHARS;
}
else if ("25".equals(buf)) {
execTextCursorDisableModeShow();
escBuf.clear();
mode = Mode.CHARS;
}
break;
mode = Mode.CHARS;
}
}
}
protected void processOsc(byte c) {
switch (c) {
default:
escBuf.put(c);
break;
case ';':
if (Set.of("0", "2").contains(readAndClearEscBuf())) {
mode = Mode.WINDOW_TITLE;
escBuf.clear();
break;
default -> escBuf.put(c);
case ';' -> {
switch (readAndClearEscBuf()) {
case "0", "2" -> mode = Mode.WINDOW_TITLE;
default -> throw new AssertionError();
}
throw new AssertionError();
}
}
}
protected void processWindowTitle(byte c) {
switch (c) {
default:
titleBuf.put(c);
break;
case 0x07: // bell, even though MSDN says longer form preferred
default -> titleBuf.put(c);
case 0x07 -> { // bell, even though MSDN says longer form preferred
execSetWindowTitle();
mode = Mode.CHARS;
break;
case 0x1b:
mode = Mode.WINDOW_TITLE_ESC;
break;
}
case 0x1b -> mode = Mode.WINDOW_TITLE_ESC;
}
}
protected void processWindowTitleEsc(byte c) {
switch (c) {
case '\\':
case '\\' -> {
execSetWindowTitle();
mode = Mode.CHARS;
break;
default:
throw new AssertionError("Saw <ST> ... ESC " + c + " at " + countIn);
}
default -> throw new AssertionError("Saw <ST> ... ESC " + c + " at " + countIn);
}
}
@@ -455,6 +408,30 @@ public class AnsiBufferedInputStream extends InputStream {
// Don't care
}
protected void execEnableFocusReport() {
// Don't care
}
protected void execDisableFocusReport() {
// Don't care
}
protected void execEnableBracketedPasteMode() {
// Don't care
}
protected void execDisableBracketedPasteMode() {
// Don't care
}
protected void execEnableWin32InputMode() {
// Don't care
}
protected void execDisableWin32InputMode() {
// Don't care
}
protected void execEraseInDisplay() {
// Because I have only one line, right?
execEraseInLine();
@@ -462,15 +439,10 @@ public class AnsiBufferedInputStream extends InputStream {
protected void execEraseInLine() {
switch (parseNumericBuffer()) {
case 0:
Arrays.fill(lineBuf.array(), lineBuf.position(), lineBuf.capacity(), (byte) 0);
break;
case 1:
Arrays.fill(lineBuf.array(), 0, lineBuf.position() + 1, (byte) 0);
break;
case 2:
Arrays.fill(lineBuf.array(), (byte) 0);
break;
case 0 -> Arrays.fill(lineBuf.array(), lineBuf.position(), lineBuf.capacity(),
(byte) 0);
case 1 -> Arrays.fill(lineBuf.array(), 0, lineBuf.position() + 1, (byte) 0);
case 2 -> Arrays.fill(lineBuf.array(), (byte) 0);
}
}

View File

@@ -15,6 +15,7 @@
*/
package agent;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.*;
import java.io.*;
@@ -47,6 +48,7 @@ import ghidra.framework.plugintool.util.*;
import ghidra.pty.*;
import ghidra.pty.PtyChild.Echo;
import ghidra.pty.testutil.DummyProc;
import ghidra.pty.windows.AnsiBufferedInputStream;
import ghidra.trace.model.Trace;
import ghidra.trace.model.target.schema.PrimitiveTraceObjectSchema.MinimalSchemaContext;
import ghidra.trace.model.target.schema.TraceObjectSchema.SchemaName;
@@ -107,11 +109,14 @@ public class TraceRmiPythonClientTest extends AbstractGhidraHeadedDebuggerTest {
protected Path getPathToPython() {
try {
return Paths.get(DummyProc.which("python3"));
String py3path = DummyProc.which("python3");
if (py3path != null && !py3path.contains("msys")) {
return Paths.get(py3path);
}
}
catch (RuntimeException e) {
return Paths.get(DummyProc.which("python"));
}
return Paths.get(DummyProc.which("python"));
}
@Before
@@ -119,6 +124,7 @@ public class TraceRmiPythonClientTest extends AbstractGhidraHeadedDebuggerTest {
traceRmi = addPlugin(tool, TraceRmiPlugin.class);
pathToPython = getPathToPython();
Msg.info(this, "Using python: %s".formatted(pathToPython));
}
protected void addAllDebuggerPlugins() throws PluginException {
@@ -180,6 +186,14 @@ public class TraceRmiPythonClientTest extends AbstractGhidraHeadedDebuggerTest {
protected ExecInPy execInPy(String script) throws IOException {
Map<String, String> env = new HashMap<>(System.getenv());
setPythonPath(env);
/**
* A new REPL was instroduced in Python 3.13. Unfortunately, the REPL is in play when we use
* a PTY, because it assumes that is a human. It will automatically insert indentation after
* pressing ENTER, which really goofs up our input. (It's worth noting, this happens even
* when copy-pasting a code block from notepad, which seems like a bug on their part.) This
* environment variable (at least for the moment) disables that new REPL.
*/
env.put("PYTHON_BASIC_REPL", "1");
Pty pty = PtyFactory.local().openpty();
PtySession session =
@@ -187,7 +201,7 @@ public class TraceRmiPythonClientTest extends AbstractGhidraHeadedDebuggerTest {
ByteArrayOutputStream out = new ByteArrayOutputStream();
new Thread(() -> {
InputStream is = pty.getParent().getInputStream();
InputStream is = new AnsiBufferedInputStream(pty.getParent().getInputStream());
byte[] buf = new byte[1024];
while (true) {
try {
@@ -203,7 +217,12 @@ public class TraceRmiPythonClientTest extends AbstractGhidraHeadedDebuggerTest {
}).start();
PrintWriter stdin = new PrintWriter(pty.getParent().getOutputStream());
script.lines().forEach(stdin::println); // to transform newlines.
/**
* Because we're using a pty, we need to use CR instead of LF, i.e., to simulate the user
* pressing ENTER.
*/
script = script.replace("\n", "\r");
stdin.write(script);
stdin.flush();
return new ExecInPy(session, stdin, CompletableFuture.supplyAsync(() -> {
try {