GP-3800: Get TerminalService working on Windows, too.

This commit is contained in:
Dan 2023-09-05 13:52:35 -04:00
parent 5bed356fd2
commit a548e54075
37 changed files with 834 additions and 217 deletions

View file

@ -79,6 +79,11 @@ public class GdbManagerImpl implements GdbManager {
private static final String GDB_IS_TERMINATING = "GDB is terminating";
public static final int MAX_CMD_LEN = 4094; // Account for longest possible line end
private static final boolean IS_WINDOWS =
OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS;
private static final short PTY_COLS = IS_WINDOWS ? Short.MAX_VALUE : 0;
private static final short PTY_ROWS = IS_WINDOWS ? (short) 1 : 0;
private String maintInfoSectionsCmd = GdbModuleImpl.MAINT_INFO_SECTIONS_CMD_V11;
private Pattern fileLinePattern = GdbModuleImpl.OBJECT_FILE_LINE_PATTERN_V11;
private Pattern sectionLinePattern = GdbModuleImpl.OBJECT_SECTION_LINE_PATTERN_V10;
@ -119,7 +124,7 @@ public class GdbManagerImpl implements GdbManager {
InputStream inputStream = pty.getParent().getInputStream();
// TODO: This should really only be applied to the MI2 console
// But, we don't know what we have until we read it....
if (OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS) {
if (IS_WINDOWS) {
inputStream = new AnsiBufferedInputStream(inputStream);
}
this.reader = new BufferedReader(new InputStreamReader(inputStream));
@ -652,7 +657,8 @@ public class GdbManagerImpl implements GdbManager {
executor = Executors.newSingleThreadExecutor();
if (gdbCmd != null) {
iniThread = new PtyThread(ptyFactory.openpty(), Channel.STDOUT, null);
iniThread =
new PtyThread(ptyFactory.openpty(PTY_COLS, PTY_ROWS), Channel.STDOUT, null);
Msg.info(this, "Starting gdb with: " + fullargs);
gdb =
@ -708,7 +714,7 @@ public class GdbManagerImpl implements GdbManager {
}
}
else {
Pty mi2Pty = ptyFactory.openpty();
Pty mi2Pty = ptyFactory.openpty(Short.MAX_VALUE, (short) 1);
String mi2PtyName = mi2Pty.getChild().nullSession(Echo.OFF);
Msg.info(this, "Agent is waiting for GDB/MI v2 interpreter at " + mi2PtyName);
mi2Thread = new PtyThread(mi2Pty, Channel.STDOUT, Interpreter.MI2);

View file

@ -40,7 +40,6 @@ import generic.ULongSpan.ULongSpanSet;
import ghidra.async.AsyncReference;
import ghidra.dbg.testutil.DummyProc;
import ghidra.pty.PtyFactory;
import ghidra.pty.linux.LinuxPtyFactory;
import ghidra.test.AbstractGhidraHeadlessIntegrationTest;
import ghidra.util.Msg;
import ghidra.util.SystemUtilities;
@ -62,8 +61,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
}
protected PtyFactory getPtyFactory() {
// TODO: Choose by host OS
return new LinuxPtyFactory();
return PtyFactory.local();
}
protected abstract CompletableFuture<Void> startManager(GdbManager manager);

View file

@ -21,8 +21,6 @@ import java.util.concurrent.CompletableFuture;
import org.junit.Ignore;
import agent.gdb.manager.GdbManager;
import ghidra.pty.PtyFactory;
import ghidra.pty.windows.ConPtyFactory;
@Ignore("Need compatible version on CI")
public class SpawnedWindowsMi2GdbManagerTest extends AbstractGdbManagerTest {
@ -36,10 +34,4 @@ public class SpawnedWindowsMi2GdbManagerTest extends AbstractGdbManagerTest {
throw new AssertionError(e);
}
}
@Override
protected PtyFactory getPtyFactory() {
// TODO: Choose by host OS
return new ConPtyFactory();
}
}

View file

@ -39,11 +39,11 @@ public class TerminalGhidraScript extends GhidraScript {
protected void displayInTerminal(PtyParent parent, Runnable waiter) throws PluginException {
TerminalService terminalService = ensureTerminalService();
try (Terminal term = terminalService.createWithStreams(Charset.forName("US-ASCII"),
try (Terminal term = terminalService.createWithStreams(Charset.forName("UTF-8"),
parent.getInputStream(), parent.getOutputStream())) {
term.addTerminalListener(new TerminalListener() {
@Override
public void resized(int cols, int rows) {
public void resized(short cols, short rows) {
parent.setWindowSize(cols, rows);
}
});

View file

@ -56,12 +56,62 @@ public class DefaultTerminal implements Terminal {
}
@Override
public void setFixedSize(int rows, int cols) {
provider.setFixedSize(rows, cols);
public void setFixedSize(short cols, short rows) {
provider.setFixedSize(cols, rows);
}
@Override
public void setDynamicSize() {
provider.setDyanmicSize();
}
@Override
public int getColumns() {
return provider.getColumns();
}
@Override
public int getRows() {
return provider.getRows();
}
@Override
public void setMaxScrollBackRows(int rows) {
provider.setMaxScrollBackRows(rows);
}
@Override
public int getScrollBackRows() {
return provider.getScrollBackRows();
}
@Override
public String getDisplayText() {
return getRangeText(0, 0, getColumns(), getRows());
}
@Override
public String getFullText() {
return getRangeText(0, -getScrollBackRows(), getColumns(), getRows());
}
@Override
public String getLineText(int line) {
return getRangeText(0, line, getColumns(), line);
}
@Override
public String getRangeText(int startCol, int startLine, int endCol, int endLine) {
return provider.getRangeText(startCol, startLine, endCol, endLine);
}
@Override
public int getCursorColumn() {
return provider.getCursorColumn();
}
@Override
public int getCursorRow() {
return provider.getCursorRow();
}
}

View file

@ -21,6 +21,7 @@ import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.*;
import ghidra.app.plugin.core.terminal.vt.VtHandler.KeyMode;
import ghidra.util.Msg;
/**
@ -45,24 +46,32 @@ public abstract class TerminalAwtEventEncoder {
}
public static final byte ESC = (byte) 0x1b;
public static final byte FUNC = (byte) 0x4f;
public static final byte[] CODE_INSERT = vtseq(2);
public static final byte[] CODE_DELETE = vtseq(3);
public static final byte[] CODE_HOME = { ESC, '[', 'H' };
public static final byte[] CODE_END = { ESC, '[', 'F' };
// Believe it or not, \r is ENTER on both Windows and Linux!
public static final byte[] CODE_ENTER = { '\r' };
public static final byte[] CODE_PAGE_UP = vtseq(5);
public static final byte[] CODE_PAGE_DOWN = vtseq(6);
public static final byte[] CODE_NUMPAD5 = { ESC, '[', 'E' };
public static final byte[] CODE_UP = { ESC, FUNC, 'A' };
public static final byte[] CODE_DOWN = { ESC, FUNC, 'B' };
public static final byte[] CODE_RIGHT = { ESC, FUNC, 'C' };
public static final byte[] CODE_LEFT = { ESC, FUNC, 'D' };
public static final byte[] CODE_F1 = { ESC, FUNC, 'P' };
public static final byte[] CODE_F2 = { ESC, FUNC, 'Q' };
public static final byte[] CODE_F3 = { ESC, FUNC, 'R' };
public static final byte[] CODE_F4 = { ESC, FUNC, 'S' };
public static final byte[] CODE_UP_NORMAL = { ESC, '[', 'A' };
public static final byte[] CODE_DOWN_NORMAL = { ESC, '[', 'B' };
public static final byte[] CODE_RIGHT_NORMAL = { ESC, '[', 'C' };
public static final byte[] CODE_LEFT_NORMAL = { ESC, '[', 'D' };
public static final byte[] CODE_UP_APPLICATION = { ESC, 'O', 'A' };
public static final byte[] CODE_DOWN_APPLICATION = { ESC, 'O', 'B' };
public static final byte[] CODE_RIGHT_APPLICATION = { ESC, 'O', 'C' };
public static final byte[] CODE_LEFT_APPLICATION = { ESC, 'O', 'D' };
public static final byte[] CODE_HOME_NORMAL = { ESC, '[', 'H' };
public static final byte[] CODE_END_NORMAL = { ESC, '[', 'F' };
public static final byte[] CODE_HOME_APPLICATION = { ESC, 'O', 'H' };
public static final byte[] CODE_END_APPLICATION = { ESC, 'O', 'F' };
public static final byte[] CODE_F1 = { ESC, '[', '1', 'P' };
public static final byte[] CODE_F2 = { ESC, '[', '1', 'Q' };
public static final byte[] CODE_F3 = { ESC, '[', '1', 'R' };
public static final byte[] CODE_F4 = { ESC, '[', '1', 'S' };
public static final byte[] CODE_F5 = vtseq(15);
public static final byte[] CODE_F6 = vtseq(17);
public static final byte[] CODE_F7 = vtseq(18);
@ -155,22 +164,23 @@ public abstract class TerminalAwtEventEncoder {
}
}
protected byte[] getAnsiKeyCode(KeyEvent e) {
protected byte[] getAnsiKeyCode(KeyEvent e, KeyMode cursorMode, KeyMode keypadMode) {
if (e.getModifiersEx() != 0) {
return getModifiedAnsiKeyCode(e);
}
return switch (e.getKeyCode()) {
case KeyEvent.VK_INSERT -> CODE_INSERT;
// NB. CODE_DELETE is handled in keyTyped
case KeyEvent.VK_HOME -> CODE_HOME;
case KeyEvent.VK_END -> CODE_END;
// Yes, HOME and END are considered CURSOR keys
case KeyEvent.VK_HOME -> cursorMode.choose(CODE_HOME_NORMAL, CODE_HOME_APPLICATION);
case KeyEvent.VK_END -> cursorMode.choose(CODE_END_NORMAL, CODE_END_APPLICATION);
case KeyEvent.VK_PAGE_UP -> CODE_PAGE_UP;
case KeyEvent.VK_PAGE_DOWN -> CODE_PAGE_DOWN;
case KeyEvent.VK_NUMPAD5 -> CODE_NUMPAD5;
case KeyEvent.VK_UP -> CODE_UP;
case KeyEvent.VK_DOWN -> CODE_DOWN;
case KeyEvent.VK_RIGHT -> CODE_RIGHT;
case KeyEvent.VK_LEFT -> CODE_LEFT;
case KeyEvent.VK_UP -> cursorMode.choose(CODE_UP_NORMAL, CODE_UP_APPLICATION);
case KeyEvent.VK_DOWN -> cursorMode.choose(CODE_DOWN_NORMAL, CODE_DOWN_APPLICATION);
case KeyEvent.VK_RIGHT -> cursorMode.choose(CODE_RIGHT_NORMAL, CODE_RIGHT_APPLICATION);
case KeyEvent.VK_LEFT -> cursorMode.choose(CODE_LEFT_NORMAL, CODE_LEFT_APPLICATION);
case KeyEvent.VK_F1 -> CODE_F1;
case KeyEvent.VK_F2 -> CODE_F2;
case KeyEvent.VK_F3 -> CODE_F3;
@ -196,8 +206,8 @@ public abstract class TerminalAwtEventEncoder {
};
}
public void keyPressed(KeyEvent e) {
byte[] bytes = getAnsiKeyCode(e);
public void keyPressed(KeyEvent e, KeyMode cursorKeyMode, KeyMode keypadMode) {
byte[] bytes = getAnsiKeyCode(e, cursorKeyMode, keypadMode);
bb.put(bytes);
generateBytesExc();
}
@ -278,6 +288,10 @@ public abstract class TerminalAwtEventEncoder {
public void sendChar(char c) {
switch (c) {
case 0x0a:
bb.put(CODE_ENTER);
generateBytesExc();
break;
case 0x7f:
bb.put(CODE_DELETE);
generateBytesExc();
@ -296,7 +310,9 @@ public abstract class TerminalAwtEventEncoder {
protected void generateBytesExc() {
bb.flip();
try {
generateBytes(bb);
if (bb.hasRemaining()) {
generateBytes(bb);
}
}
catch (Throwable t) {
Msg.error(this, "Error generating bytes: " + t, t);

View file

@ -87,10 +87,13 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler {
protected VtBuffer buffer = bufPrimary;
// Flags for what's been enabled
protected boolean showCursor;
protected boolean bracketedPaste;
protected boolean reportMousePress;
protected boolean reportMouseRelease;
protected boolean reportFocus;
protected KeyMode cursorKeyMode = KeyMode.NORMAL;
protected KeyMode keypadMode = KeyMode.NORMAL;
private Object lock = new Object();
@ -133,6 +136,8 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler {
reportMousePress = false;
reportMouseRelease = false;
reportFocus = false;
cursorKeyMode = KeyMode.NORMAL;
keypadMode = KeyMode.NORMAL;
}
public void processInput(ByteBuffer buffer) {
@ -275,7 +280,7 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler {
try {
// A little strange using both unicode and vt charsets....
buffer.putChar(curVtCharset.mapChar(cb.get()));
buffer.moveCursorRight(1);
buffer.moveCursorRight(1, true, showCursor);
}
catch (Throwable t) {
Msg.error(this, "Error handling character: " + t, t);
@ -291,7 +296,7 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler {
@Override
public void handleBackSpace() {
buffer.moveCursorLeft(1);
buffer.moveCursorLeft(1, true);
}
@Override
@ -308,7 +313,7 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler {
@Override
public void handleLineFeed() {
buffer.moveCursorDown(1);
buffer.moveCursorDown(1, true);
}
@Override
@ -392,15 +397,17 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler {
}
@Override
public void handleApplicationCursorKeys(boolean en) {
// Not sure what this means. Ignore for now.
Msg.trace(this, "TODO: handleApplicationCursorKeys: " + en);
public void handleCursorKeyMode(KeyMode mode) {
this.cursorKeyMode = mode;
}
@Override
public void handleApplicationKeypad(boolean en) {
// Not sure what this means. Ignore for now.
Msg.trace(this, "TODO: handleApplicationKeypad: " + en);
public void handleKeypadMode(KeyMode mode) {
/**
* This will be difficult to implement in Swing/AWT, since the OS and Java will already have
* mapped the key, including incorporating the NUMLOCK state. Ignore until it matters.
*/
Msg.trace(this, "TODO: handleKeypadMode: " + mode);
}
@Override
@ -417,6 +424,11 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler {
@Override
public void handleShowCursor(boolean show) {
this.showCursor = show;
if (show) {
bufPrimary.checkVerticalScroll();
bufAlternate.checkVerticalScroll();
}
panel.fieldPanel.setCursorOn(show);
}
@ -470,13 +482,13 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler {
buffer.moveCursorUp(n);
return;
case DOWN:
buffer.moveCursorDown(n);
buffer.moveCursorDown(n, false);
return;
case FORWARD:
buffer.moveCursorRight(n);
buffer.moveCursorRight(n, false, showCursor);
return;
case BACK:
buffer.moveCursorLeft(n);
buffer.moveCursorLeft(n, false);
return;
}
}
@ -603,6 +615,10 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler {
return buffer.getCurX();
}
public int resetCursorBottom() {
return buffer.resetBottomY();
}
public int getCols() {
return buffer.getCols();
}
@ -630,4 +646,8 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler {
layoutCache.clear();
buildLayouts();
}
public void setMaxScrollBackSize(int rows) {
bufPrimary.setMaxScrollBack(rows);
}
}

View file

@ -28,7 +28,7 @@ public interface TerminalListener {
* @param cols the number of columns
* @param rows the number of rows
*/
default void resized(int cols, int rows) {
default void resized(short cols, short rows) {
}
/**

View file

@ -209,7 +209,7 @@ public class TerminalPanel extends JPanel implements FieldLocationListener, Fiel
if (provider.isLocalActionKeyBinding(e)) {
return; // Do not consume, so action can take it
}
eventEncoder.keyPressed(e);
eventEncoder.keyPressed(e, model.cursorKeyMode, model.keypadMode);
e.consume();
}
@ -324,7 +324,7 @@ public class TerminalPanel extends JPanel implements FieldLocationListener, Fiel
terminalListeners.remove(listener);
}
protected void notifyTerminalResized(int cols, int rows) {
protected void notifyTerminalResized(short cols, short rows) {
for (TerminalListener l : terminalListeners) {
try {
l.resized(cols, rows);
@ -530,7 +530,7 @@ public class TerminalPanel extends JPanel implements FieldLocationListener, Fiel
fieldPanel.setCursorPosition(BigInteger.valueOf(model.getCursorRow() + scrollBack), 0, 0,
model.getCursorColumn());
if (scroll) {
fieldPanel.scrollToCursor();
fieldPanel.scrollTo(new FieldLocation(model.resetCursorBottom() + scrollBack));
}
}
@ -664,14 +664,14 @@ public class TerminalPanel extends JPanel implements FieldLocationListener, Fiel
protected void resizeTerminalToWindow() {
Rectangle bounds = scroller.getViewportBorderBounds();
int rows = bounds.height / metrics.getHeight();
int cols = bounds.width / metrics.charWidth('M');
resizeTerminal(rows, cols);
int rows = bounds.height / metrics.getHeight();
resizeTerminal((short) cols, (short) rows);
}
protected void resizeTerminal(int rows, int cols) {
if (model.resizeTerminal(cols, rows)) {
notifyTerminalResized(model.getCols(), model.getRows());
protected void resizeTerminal(short cols, short rows) {
if (model.resizeTerminal(Short.toUnsignedInt(cols), Short.toUnsignedInt(rows))) {
notifyTerminalResized((short) model.getCols(), (short) model.getRows());
}
}
@ -683,14 +683,14 @@ public class TerminalPanel extends JPanel implements FieldLocationListener, Fiel
* needed. If the terminal size changes as a result of this call,
* {@link TerminalListener#resized(int, int)} is invoked.
*
* @param rows the number of rows
* @param cols the number of columns
* @param rows the number of rows
*/
public void setFixedTerminalSize(int rows, int cols) {
public void setFixedTerminalSize(short cols, short rows) {
this.fixedSize = true;
scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
resizeTerminal(rows, cols);
resizeTerminal(cols, rows);
}
/**
@ -709,4 +709,20 @@ public class TerminalPanel extends JPanel implements FieldLocationListener, Fiel
scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
resizeTerminalToWindow();
}
public int getColumns() {
return model.getCols();
}
public int getRows() {
return model.getRows();
}
public int getCursorColumn() {
return model.getCursorColumn();
}
public int getCursorRow() {
return model.getCursorRow();
}
}

View file

@ -29,6 +29,7 @@ import ghidra.app.services.*;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.util.Msg;
import ghidra.util.Swing;
/**
* The plugin that provides {@link TerminalService}
@ -52,13 +53,15 @@ public class TerminalPlugin extends Plugin implements TerminalService {
}
public TerminalProvider createProvider(Charset charset, VtOutput outputCb) {
TerminalProvider provider = new TerminalProvider(this, charset);
provider.setOutputCallback(outputCb);
provider.addToTool();
provider.setVisible(true);
providers.add(provider);
provider.setClipboardService(clipboardService);
return provider;
return Swing.runNow(() -> {
TerminalProvider provider = new TerminalProvider(this, charset);
provider.setOutputCallback(outputCb);
provider.addToTool();
provider.setVisible(true);
providers.add(provider);
provider.setClipboardService(clipboardService);
return provider;
});
}
@Override
@ -72,6 +75,7 @@ public class TerminalPlugin extends Plugin implements TerminalService {
return new ThreadedTerminal(createProvider(charset, buf -> {
while (buf.hasRemaining()) {
try {
//ThreadedTerminal.printBuffer(">> ", buf);
channel.write(buf);
}
catch (IOException e) {

View file

@ -299,11 +299,42 @@ public class TerminalProvider extends ComponentProviderAdapter {
return false;
}
public void setFixedSize(int rows, int cols) {
panel.setFixedTerminalSize(rows, cols);
public void setFixedSize(short cols, short rows) {
panel.setFixedTerminalSize(cols, rows);
}
public void setDyanmicSize() {
panel.setDynamicTerminalSize();
}
public int getColumns() {
return panel.getColumns();
}
public int getRows() {
return panel.getRows();
}
public void setMaxScrollBackRows(int rows) {
panel.model.setMaxScrollBackSize(rows);
}
public int getScrollBackRows() {
return panel.model.getScrollBackSize();
}
public String getRangeText(int startCol, int startLine, int endCol, int endLine) {
int scrollBack = getScrollBackRows();
return panel.getSelectedText(new FieldRange(
new FieldLocation(startLine + scrollBack, 0, 0, startCol),
new FieldLocation(endLine + scrollBack, 0, 0, endCol)));
}
public int getCursorColumn() {
return panel.getCursorColumn();
}
public int getCursorRow() {
return panel.getCursorRow();
}
}

View file

@ -116,7 +116,8 @@ public class TerminalTextField implements TextField {
g.setColor(cursorColor);
int x = startX + findX(cursorLoc.col());
g.drawRect(x, -getHeightAbove(), em - 1, getHeight() - 1);
g.drawRect(x + 1, -getHeightAbove() + 1, em - 3, getHeight() - 3);
// This technique looks ugly with display scaling
//g.drawRect(x + 1, -getHeightAbove() + 1, em - 3, getHeight() - 3);
}
}

View file

@ -61,11 +61,10 @@ public class ThreadedTerminal extends DefaultTerminal {
super.close();
}
@SuppressWarnings("unused") // diagnostic
private void printBuffer() {
byte[] bytes = new byte[buffer.remaining()];
buffer.get(buffer.position(), bytes);
//System.err.println("<< " + NumericUtilities.convertBytesToString(bytes, ":"));
static void printBuffer(String prefix, ByteBuffer bb) {
byte[] bytes = new byte[bb.remaining()];
bb.get(bb.position(), bytes);
System.err.print(prefix);
try {
String str = new String(bytes, "US-ASCII");
for (char c : str.toCharArray()) {
@ -93,7 +92,7 @@ public class ThreadedTerminal extends DefaultTerminal {
return;
}
buffer.flip();
//printBuffer();
//printBuffer("<< ", buffer);
synchronized (buffer) {
provider.processInput(buffer);
}

View file

@ -41,6 +41,7 @@ public class VtBuffer {
protected int curY;
protected int savedX;
protected int savedY;
protected int bottomY; // for scrolling UI after an update
protected int scrollStart;
protected int scrollEnd; // exclusive
@ -128,6 +129,8 @@ public class VtBuffer {
if (c == 0) {
return;
}
checkVerticalScroll();
// At this point, we have no choice but to wrap
lines.get(curY).putChar(curX, c, curAttrs);
}
@ -136,7 +139,7 @@ public class VtBuffer {
*/
public void tab() {
int n = TAB_WIDTH + (-curX % TAB_WIDTH);
moveCursorRight(n);
moveCursorRight(n, false, false);
}
/**
@ -147,7 +150,7 @@ public class VtBuffer {
return;
}
int n = (curX - 1) % TAB_WIDTH + 1;
moveCursorLeft(n);
moveCursorLeft(n, false);
}
/**
@ -157,6 +160,13 @@ public class VtBuffer {
* This does <em>not</em> move the cursor down.
*/
public void carriageReturn() {
if (curX == 0) {
return;
}
int prevY = curY - 1;
if (prevY >= 0 && prevY < lines.size()) {
lines.get(prevY).wrappedToNext = false;
}
curX = 0;
}
@ -236,57 +246,82 @@ public class VtBuffer {
* that the cursor remains in the display. The value of n must be positive, otherwise behavior
* is undefined. To move the cursor up, use {@link #moveCursorUp(int)}.
*
* @param n
* <p>
* ConPty has a habit of moving the cursor past the end of the current line before sending CRLF.
* (Though, I imagine there are other applications that might do this.) The {@code dedupWrap}
* parameter is made to accommodate this. If it is set, n is a single line, and the previous
* line was wrapped, then this does nothing more than remove the wrapped flag from the previous
* line.
*
* @param n the number of lines to move down
* @param dedupWrap whether to detect and ignore a line feed after wrapping
*/
public void moveCursorDown(int n) {
curY += n;
checkVerticalScroll();
public void moveCursorDown(int n, boolean dedupWrap) {
int prevY = curY - 1;
if (dedupWrap && n == 1 && prevY >= 0 && prevY < lines.size() &&
lines.get(prevY).wrappedToNext) {
lines.get(prevY).wrappedToNext = false;
}
else {
curY += n;
bottomY = Math.max(bottomY, curY);
checkVerticalScroll();
}
}
/**
* Move the cursor left (backward) n columns
*
* <p>
* If the cursor would move left of the display, it is instead moved to the far right of the
* previous row, unless the cursor is already on the top row, in which case, it will be placed
* in the top-left corner of the display. NOTE: If the cursor is moved to the previous row, no
* heed is given to "leftovers." It doesn't matter how far to the left the cursor would have
* been; it is moved to the far right column and exactly one row up. The value of n must be
* positive, otherwise behavior is undefined. To move the cursor right, use
* {@link #moveCursorRight(int)}.
* The cursor is clamped into the display. If wrap is specified, the cursor would exceed the
* left side of the display, and the previous line was wrapped onto the current line, then the
* cursor will instead be moved to the end of the previous line. (It doesn't matter how far the
* cursor would exceed the left; it moves up at most one line.) The value of n must be positive,
* otherwise behavior is undefined. To move the cursor right, use {@link #moveCursorRight(int)}.
*
* @param n the number of columns
* @param wrap whether to wrap the cursor to the previous line if would exceed the left of the
* display
*/
public void moveCursorLeft(int n) {
if (curX - n >= 0) {
curX -= n;
}
else if (curY > 0) {
public void moveCursorLeft(int n, boolean wrap) {
int prevY = curY - 1;
if (wrap && curX - n < 0 && prevY >= 0 && prevY < lines.size() &&
lines.get(prevY).wrappedToNext) {
curX = cols - 1;
curY--;
lines.get(curY).wrappedToNext = false;
}
curX = Math.max(0, Math.min(curX - n, cols - 1));
}
/**
* Move the cursor right (forward) n columns
*
* <p>
* If the cursor would move right of the display, it is instead moved to the far left of the
* next row. If the cursor is already on the bottom row, the viewport is scrolled down a line.
* NOTE: If the cursor is moved to the next row, no heed is given to "leftovers." It doesn't
* matter how far to the right the cursor would have been; it is moved to the far left column
* and exactly one row down. The value of n must be positive, otherwise behavior is undefined.
* To move the cursor left, use {@link #moveCursorLeft(int)}.
* The cursor is clamped into the display. If wrap is specified and the cursor would exceed the
* right side of the display, the cursor will instead be wrapped to the start of the next line,
* possibly scrolling the viewport down. (It doesn't matter how far the cursor exceeds the
* right; the cursor moves down exactly one line.) The value of n must be positive, otherwise
* behavior is undefined. To move the cursor left, use {@link #moveCursorLeft(int)}.
*
* @param n the number of columns
* @param wrap whether to wrap the cursor to the next line if it would exceed the right of the
* display
*/
public void moveCursorRight(int n) {
curX += n;
if (curX >= cols) {
public void moveCursorRight(int n, boolean wrap, boolean isCursorShowing) {
if (wrap && curX + n >= cols) {
checkVerticalScroll();
curX = 0;
lines.get(curY).wrappedToNext = true;
curY++;
bottomY = Math.max(bottomY, curY);
if (isCursorShowing) {
checkVerticalScroll();
}
}
else {
curX = Math.max(0, Math.min(curX + n, cols - 1));
}
checkVerticalScroll();
}
/**
@ -313,6 +348,7 @@ public class VtBuffer {
public void restoreCursorPos() {
curX = savedX;
curY = savedY;
bottomY = Math.max(bottomY, curY);
}
/**
@ -326,8 +362,9 @@ public class VtBuffer {
* @param col the desired column, 0 up, left to right
*/
public void moveCursor(int row, int col) {
this.curX = Math.max(0, Math.min(cols - 1, col));
this.curY = Math.max(0, Math.min(rows - 1, row));
curX = Math.max(0, Math.min(cols - 1, col));
curY = Math.max(0, Math.min(rows - 1, row));
bottomY = Math.max(bottomY, curY);
}
/**
@ -370,6 +407,9 @@ public class VtBuffer {
public void erase(Erasure erasure) {
switch (erasure) {
case TO_DISPLAY_END:
if (curY >= lines.size()) {
return;
}
for (int y = curY; y < rows; y++) {
VtLine line = lines.get(y);
if (y == curY) {
@ -403,12 +443,21 @@ public class VtBuffer {
scrollBack.clear();
return;
case TO_LINE_END:
if (curY >= lines.size()) {
return;
}
lines.get(curY).clearToEnd(curX);
return;
case TO_LINE_START:
if (curY >= lines.size()) {
return;
}
lines.get(curY).clearToStart(curX, curAttrs);
return;
case FULL_LINE:
if (curY >= lines.size()) {
return;
}
lines.get(curY).clear();
return;
}
@ -462,6 +511,9 @@ public class VtBuffer {
* @param n the number of blanks to insert.
*/
public void insertChars(int n) {
if (curY >= lines.size()) {
return;
}
lines.get(curY).insert(curX, n);
}
@ -475,6 +527,9 @@ public class VtBuffer {
* @param n the number of characters to delete
*/
public void deleteChars(int n) {
if (curY >= lines.size()) {
return;
}
lines.get(curY).delete(curX, curX + n);
}
@ -488,6 +543,9 @@ public class VtBuffer {
* @param n the number of characters to erase
*/
public void eraseChars(int n) {
if (curY >= lines.size()) {
return;
}
lines.get(curY).erase(curX, curX + n, curAttrs);
}
@ -750,4 +808,10 @@ public class VtBuffer {
}
return buf.toString();
}
public int resetBottomY() {
int ret = bottomY;
bottomY = curY;
return ret;
}
}

View file

@ -125,6 +125,7 @@ public interface VtHandler {
public static final byte[] Q7 = ascii("?7");
public static final byte[] Q12 = ascii("?12");
public static final byte[] Q25 = ascii("?25");
public static final byte[] Q47 = ascii("?47");
public static final byte[] Q1000 = ascii("?1000");
public static final byte[] Q1004 = ascii("?1004");
public static final byte[] Q1034 = ascii("?1034");
@ -502,6 +503,29 @@ public interface VtHandler {
}
}
/**
* For cursor and keypad, specifies normal or application mode
*
* <p>
* This affects the codes sent by the terminal.
*/
public enum KeyMode {
NORMAL {
@Override
public <T> T choose(T normal, T application) {
return normal;
}
},
APPLICATION {
@Override
public <T> T choose(T normal, T application) {
return application;
}
};
public abstract <T> T choose(T normal, T application);
}
/**
* Check if the given buffer's contents are equal to that of the given array
*
@ -663,7 +687,7 @@ public interface VtHandler {
handleInsertMode(en);
}
else if (bufEq(csiParam, Q1)) {
handleApplicationCursorKeys(en);
handleCursorKeyMode(en ? KeyMode.APPLICATION : KeyMode.NORMAL);
}
else if (bufEq(csiParam, Q7)) {
handleAutoWrapMode(en);
@ -674,6 +698,10 @@ public interface VtHandler {
else if (bufEq(csiParam, Q25)) {
handleShowCursor(en);
}
else if (bufEq(csiParam, Q47)) {
// NB. Same as 1047?
handleAltScreenBuffer(en, false);
}
else if (bufEq(csiParam, Q1000)) {
handleReportMouseEvents(en, en);
}
@ -684,6 +712,7 @@ public interface VtHandler {
handleMetaKey(en);
}
else if (bufEq(csiParam, Q1047)) {
// NB. Same as 47?
handleAltScreenBuffer(en, false);
}
else if (bufEq(csiParam, Q1048)) {
@ -981,6 +1010,11 @@ public interface VtHandler {
// TODO: 104;c;c;c... is color reset. I've not implemented setting them, though.
// No c given = reset all
// Windows includes the null terminator
static String truncateAtNull(String str) {
return str.split("\000", 2)[0];
}
/**
* Handle an OSC sequence
*
@ -994,7 +1028,7 @@ public interface VtHandler {
matcher = PAT_OSC_WINDOW_TITLE.matcher(paramStr);
if (matcher.matches()) {
handleWindowTitle(matcher.group("title"));
handleWindowTitle(truncateAtNull(matcher.group("title")));
return;
}
@ -1349,18 +1383,18 @@ public interface VtHandler {
void handleInsertMode(boolean en);
/**
* Toggle application handling of the cursor keys
* Toggle cursor key mode
*
* @param en true (default) for application control, false for local control
* @param mode the key mode
*/
void handleApplicationCursorKeys(boolean en);
void handleCursorKeyMode(KeyMode mode);
/**
* Toggle application handling of the keypad
* Toggle keypad mode
*
* @param en true for application control, false for local control
* @param mode the key mode
*/
void handleApplicationKeypad(boolean en);
void handleKeypadMode(KeyMode mode);
/**
* Toggle auto-wrap mode

View file

@ -22,6 +22,7 @@ public class VtLine {
protected int cols;
protected int len;
protected char[] chars;
protected boolean wrappedToNext;
private VtAttributes[] cellAttrs;
/**
@ -80,6 +81,7 @@ public class VtLine {
public void putChar(int x, char c, VtAttributes attrs) {
int oldLen = len;
len = Math.max(len, x + 1);
wrappedToNext = false; // Maybe remove
for (int i = oldLen; i < x; i++) {
chars[i] = ' ';
cellAttrs[i] = VtAttributes.DEFAULTS;
@ -118,6 +120,7 @@ public class VtLine {
public void reset(int cols) {
this.cols = cols;
this.len = 0;
this.wrappedToNext = false;
if (this.cols != cols || this.chars == null) {
this.chars = new char[cols];
this.cellAttrs = new VtAttributes[cols];
@ -147,6 +150,7 @@ public class VtLine {
*/
public void clear() {
len = 0;
wrappedToNext = false;
}
/**
@ -156,6 +160,7 @@ public class VtLine {
*/
public void clearToEnd(int x) {
len = Math.min(len, x);
wrappedToNext = false;
}
/**
@ -167,6 +172,7 @@ public class VtLine {
public void clearToStart(int x, VtAttributes attrs) {
if (len <= x) {
len = 0;
wrappedToNext = false;
return;
}
for (int i = 0; i <= x; i++) {
@ -183,7 +189,8 @@ public class VtLine {
*/
public void delete(int start, int end) {
if (len <= end) {
len = start;
len = Math.min(len, start);
wrappedToNext = false;
return;
}
int shift = end - start;
@ -208,7 +215,8 @@ public class VtLine {
*/
public void erase(int start, int end, VtAttributes attrs) {
if (len <= end) {
len = start;
len = Math.min(len, start);
wrappedToNext = false;
return;
}
for (int x = start; x < end; x++) {
@ -238,6 +246,7 @@ public class VtLine {
chars[x] = ' ';
}
len = Math.min(cols, len + n);
wrappedToNext = false;
}
/**

View file

@ -16,6 +16,7 @@
package ghidra.app.plugin.core.terminal.vt;
import ghidra.app.plugin.core.terminal.vt.VtCharset.G;
import ghidra.app.plugin.core.terminal.vt.VtHandler.KeyMode;
public enum VtState {
/**
@ -61,11 +62,10 @@ public enum VtState {
case ']':
return OSC_PARAM;
case '=':
handler.handleApplicationKeypad(true);
handler.handleKeypadMode(KeyMode.APPLICATION);
return CHAR;
case '>':
// Normal keypad
handler.handleApplicationKeypad(false);
handler.handleKeypadMode(KeyMode.NORMAL);
return CHAR;
case 'D':
handler.handleScrollViewportDown(1, true);
@ -267,7 +267,8 @@ public enum VtState {
OSC_PARAM {
@Override
protected VtState handleNext(byte b, VtParser parser, VtHandler handler) {
if (0x20 <= b && b <= 0x7f) {
// For whatever reason, Windows includes the null terminator in titles
if (0x20 <= b && b <= 0x7f || b == 0) {
parser.oscParam.put(b);
return OSC_PARAM;
}

View file

@ -61,18 +61,113 @@ public interface Terminal extends AutoCloseable {
}
/**
* Set the terminal size to the given dimensions, as do <em>not</em> resize it to the window.
* Set the terminal size to the given dimensions, and do <em>not</em> resize it to the window.
*
* @param rows the number of rows
* @param cols the number of columns
* @param rows the number of rows
*/
void setFixedSize(int rows, int cols);
void setFixedSize(short cols, short rows);
/**
* Fit the terminals dimensions to the containing window.
* @see #setFixedSize(short, short)
*/
default void setFixedSize(int cols, int rows) {
setFixedSize((short) cols, (short) rows);
}
/**
* Fit the terminal's dimensions to the containing window.
*/
void setDynamicSize();
/**
* Set the maximum size of the scroll-back buffer in lines
*
* <p>
* This only affects the primary buffer. The alternate buffer has no scroll-back.
*/
void setMaxScrollBackRows(int rows);
/**
* Get the maximum number of characters in each row
*
* @return the column count
*/
int getColumns();
/**
* Get the maximum number of rows in the display (not counting scroll-back)
*
* @return the row count
*/
int getRows();
/**
* Get the number of lines in the scroll-back buffer
*
* @return the size of the buffer in lines
*/
int getScrollBackRows();
/**
* Get all the text in the terminal, including the scroll-back buffer
*
* @return the full text
*/
String getFullText();
/**
* Get the text in the terminal, excluding the scroll-back buffer
*
* @return the display text
*/
String getDisplayText();
/**
* Get the given line's text
*
* <p>
* The line at the top of the display has index 0. Lines in the scroll-back buffer have negative
* indices.
*
* @param line the index, 0 up
* @return the text in the line
*/
String getLineText(int line);
/**
* Get the text in the given range
*
* <p>
* The line at the top of the display has index 0. Lines in the scroll-back buffer have negative
* indices.
*
* @param startCol the first column to include in the starting line
* @param startLine the first line to include
* @param endCol the first column to <em>exclude</em> in the ending line
* @param endLine the last line to include
* @return the text in the given range
*/
String getRangeText(int startCol, int startLine, int endCol, int endLine);
/**
* Get the cursor's current line
*
* <p>
* Lines are indexed 0 up where the top line of the display is 0. The cursor can never be in the
* scroll-back buffer.
*
* @return the line, 0 up, top to bottom
*/
int getCursorRow();
/**
* Get the cursor's current column
*
* @return the column, 0 up, left to right
*/
int getCursorColumn();
@Override
void close();
}

View file

@ -26,6 +26,8 @@ import ghidra.pty.windows.ConPtyFactory;
* A mechanism for opening pseudo-terminals
*/
public interface PtyFactory {
short DEFAULT_COLS = 80;
short DEFAULT_ROWS = 25;
/**
* Choose a factory of local pty's for the host operating system
@ -35,11 +37,11 @@ public interface PtyFactory {
static PtyFactory local() {
switch (OperatingSystem.CURRENT_OPERATING_SYSTEM) {
case MAC_OS_X:
return new MacosPtyFactory();
return MacosPtyFactory.INSTANCE;
case LINUX:
return new LinuxPtyFactory();
return LinuxPtyFactory.INSTANCE;
case WINDOWS:
return new ConPtyFactory();
return ConPtyFactory.INSTANCE;
default:
throw new UnsupportedOperationException();
}
@ -48,10 +50,40 @@ public interface PtyFactory {
/**
* Open a new pseudo-terminal
*
* @param cols the initial width in characters, or 0 to let the system decide both dimensions
* @param rows the initial height in characters, or 0 to let the system decide both dimensions
* @return new new Pty
* @throws IOException for an I/O error, including cancellation
*/
Pty openpty() throws IOException;
Pty openpty(short cols, short rows) throws IOException;
/**
* Open a new pseudo-terminal of the default size ({@value #DEFAULT_COLS} x
* {@value #DEFAULT_ROWS})
*
* @return new new Pty
* @throws IOException for an I/O error, including cancellation
*/
default Pty openpty() throws IOException {
return openpty(DEFAULT_COLS, DEFAULT_ROWS);
}
/**
* Open a new pseudo-terminal
*
* @param cols the initial width in characters, or 0 to let the system decide both dimensions
* @param rows the initial height in characters, or 0 to let the system decide both dimensions
* @return new new Pty
* @throws IOException for an I/O error, including cancellation
*/
default Pty openpty(int cols, int rows) throws IOException {
return openpty((short) cols, (short) rows);
}
/**
* Get a human-readable description of the factory
*
* @return the description
*/
String getDescription();
}

View file

@ -19,5 +19,11 @@ package ghidra.pty;
* The parent (UNIX "master") end of a pseudo-terminal
*/
public interface PtyParent extends PtyEndpoint {
void setWindowSize(int cols, int rows);
/**
* Resize the terminal window to the given width and height, in characters
*
* @param cols the width in characters
* @param rows the height in characters
*/
void setWindowSize(short cols, short rows);
}

View file

@ -20,10 +20,16 @@ import java.io.IOException;
import ghidra.pty.Pty;
import ghidra.pty.PtyFactory;
public class LinuxPtyFactory implements PtyFactory {
public enum LinuxPtyFactory implements PtyFactory {
INSTANCE;
@Override
public Pty openpty() throws IOException {
return LinuxPty.openpty();
public Pty openpty(short cols, short rows) throws IOException {
LinuxPty pty = LinuxPty.openpty();
if (cols != 0 && rows != 0) {
pty.getParent().setWindowSize(cols, rows);
}
return pty;
}
@Override

View file

@ -24,14 +24,10 @@ public class LinuxPtyParent extends LinuxPtyEndpoint implements PtyParent {
}
@Override
public void setWindowSize(int cols, int rows) {
if (cols > 0xffff || rows > 0xffff) {
throw new IllegalArgumentException(
"Dimensions limited to unsigned shorts. Got cols=" + cols + ",rows=" + rows);
}
public void setWindowSize(short cols, short rows) {
Winsize.ByReference ws = new Winsize.ByReference();
ws.ws_col = (short) cols;
ws.ws_row = (short) rows;
ws.ws_col = cols;
ws.ws_row = rows;
ws.write();
PosixC.INSTANCE.ioctl(fd, Winsize.TIOCSWINSZ, ws.getPointer());
}

View file

@ -21,10 +21,16 @@ import ghidra.pty.Pty;
import ghidra.pty.PtyFactory;
import ghidra.pty.linux.LinuxPty;
public class MacosPtyFactory implements PtyFactory {
public enum MacosPtyFactory implements PtyFactory {
INSTANCE;
@Override
public Pty openpty() throws IOException {
return LinuxPty.openpty();
public Pty openpty(short cols, short rows) throws IOException {
LinuxPty pty = LinuxPty.openpty();
if (cols != 0 && rows != 0) {
pty.getParent().setWindowSize(cols, rows);
}
return pty;
}
@Override

View file

@ -227,12 +227,16 @@ public class GhidraSshPtyFactory implements PtyFactory {
}
@Override
public SshPty openpty() throws IOException {
public SshPty openpty(short cols, short rows) throws IOException {
if (session == null) {
session = connectAndAuthenticate();
}
try {
return new SshPty((ChannelExec) session.openChannel("exec"));
SshPty pty = new SshPty((ChannelExec) session.openChannel("exec"));
if (cols != 0 && rows != 0) {
pty.getParent().setWindowSize(cols, rows);
}
return pty;
}
catch (JSchException e) {
throw new IOException("SSH connection error", e);

View file

@ -28,7 +28,7 @@ public class SshPtyParent extends SshPtyEndpoint implements PtyParent {
}
@Override
public void setWindowSize(int cols, int rows) {
channel.setPtySize(cols, rows, 0, 0);
public void setWindowSize(short cols, short rows) {
channel.setPtySize(Short.toUnsignedInt(cols), Short.toUnsignedInt(rows), 0, 0);
}
}

View file

@ -20,24 +20,18 @@ import java.io.IOException;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinDef.DWORD;
import com.sun.jna.platform.win32.WinNT.HANDLEByReference;
import com.sun.jna.platform.win32.COM.COMUtils;
import ghidra.pty.*;
import ghidra.pty.windows.jna.ConsoleApiNative;
import ghidra.pty.windows.jna.ConsoleApiNative.COORD;
import com.sun.jna.platform.win32.COM.COMUtils;
public class ConPty implements Pty {
static final DWORD DW_ZERO = new DWORD(0);
static final DWORD DW_ONE = new DWORD(1);
static final DWORD PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = new DWORD(0x20016);
static final DWORD EXTENDED_STARTUPINFO_PRESENT =
new DWORD(Kernel32.EXTENDED_STARTUPINFO_PRESENT);
private static final COORD SIZE = new COORD();
static {
SIZE.X = Short.MAX_VALUE;
SIZE.Y = 1;
}
private final Pipe pipeToChild;
private final Pipe pipeFromChild;
@ -47,7 +41,7 @@ public class ConPty implements Pty {
private final ConPtyParent parent;
private final ConPtyChild child;
public static ConPty openpty() {
public static ConPty openpty(short cols, short rows) {
// Create communication channels
Pipe pipeToChild = Pipe.createPipe();
@ -58,8 +52,11 @@ public class ConPty implements Pty {
HANDLEByReference lphPC = new HANDLEByReference();
COORD.ByValue size = new COORD.ByValue();
size.X = cols;
size.Y = rows;
COMUtils.checkRC(ConsoleApiNative.INSTANCE.CreatePseudoConsole(
SIZE,
size,
pipeToChild.getReadHandle().getNative(),
pipeFromChild.getWriteHandle().getNative(),
DW_ZERO,
@ -76,7 +73,8 @@ public class ConPty implements Pty {
// TODO: See if this can all be combined with named pipes.
// Would be nice if that's sufficient to support new-ui
this.parent = new ConPtyParent(pipeToChild.getWriteHandle(), pipeFromChild.getReadHandle());
this.parent = new ConPtyParent(pipeToChild.getWriteHandle(), pipeFromChild.getReadHandle(),
pseudoConsoleHandle);
this.child = new ConPtyChild(pipeFromChild.getWriteHandle(), pipeToChild.getReadHandle(),
pseudoConsoleHandle);
}

View file

@ -32,11 +32,10 @@ import ghidra.pty.windows.jna.ConsoleApiNative;
import ghidra.pty.windows.jna.ConsoleApiNative.STARTUPINFOEX;
public class ConPtyChild extends ConPtyEndpoint implements PtyChild {
private final Handle pseudoConsoleHandle;
public ConPtyChild(Handle writeHandle, Handle readHandle, Handle pseudoConsoleHandle) {
super(writeHandle, readHandle);
this.pseudoConsoleHandle = pseudoConsoleHandle;
public ConPtyChild(Handle writeHandle, Handle readHandle,
PseudoConsoleHandle pseudoConsoleHandle) {
super(writeHandle, readHandle, pseudoConsoleHandle);
}
protected STARTUPINFOEX prepareStartupInfo() {

View file

@ -21,12 +21,15 @@ import java.io.OutputStream;
import ghidra.pty.PtyEndpoint;
public class ConPtyEndpoint implements PtyEndpoint {
protected InputStream inputStream;
protected OutputStream outputStream;
protected final InputStream inputStream;
protected final OutputStream outputStream;
protected final PseudoConsoleHandle pseudoConsoleHandle;
public ConPtyEndpoint(Handle writeHandle, Handle readHandle) {
public ConPtyEndpoint(Handle writeHandle, Handle readHandle,
PseudoConsoleHandle pseudoConsoleHandle) {
this.inputStream = new HandleInputStream(readHandle);
this.outputStream = new HandleOutputStream(writeHandle);
this.pseudoConsoleHandle = pseudoConsoleHandle;
}
@Override

View file

@ -20,10 +20,15 @@ import java.io.IOException;
import ghidra.pty.Pty;
import ghidra.pty.PtyFactory;
public class ConPtyFactory implements PtyFactory {
public enum ConPtyFactory implements PtyFactory {
INSTANCE;
@Override
public Pty openpty() throws IOException {
return ConPty.openpty();
public Pty openpty(short cols, short rows) throws IOException {
if (cols == 0 || rows == 0) {
return ConPty.openpty((short) 80, (short) 25);
}
return ConPty.openpty(cols, rows);
}
@Override

View file

@ -16,15 +16,15 @@
package ghidra.pty.windows;
import ghidra.pty.PtyParent;
import ghidra.util.Msg;
public class ConPtyParent extends ConPtyEndpoint implements PtyParent {
public ConPtyParent(Handle writeHandle, Handle readHandle) {
super(writeHandle, readHandle);
public ConPtyParent(Handle writeHandle, Handle readHandle,
PseudoConsoleHandle pseudoConsoleHandle) {
super(writeHandle, readHandle, pseudoConsoleHandle);
}
@Override
public void setWindowSize(int rows, int cols) {
Msg.error(this, "Pty window size not implemented on Windows");
public void setWindowSize(short cols, short rows) {
pseudoConsoleHandle.resize(rows, cols);
}
}

View file

@ -52,7 +52,7 @@ public class Handle implements AutoCloseable {
}
@Override
public void close() throws Exception {
public void close() {
cleanable.clean();
}

View file

@ -88,7 +88,8 @@ public class HandleInputStream extends InputStream {
}
@Override
public synchronized void close() throws IOException {
public void close() throws IOException {
closed = true;
handle.close();
}
}

View file

@ -16,8 +16,10 @@
package ghidra.pty.windows;
import com.sun.jna.platform.win32.WinNT.HANDLE;
import com.sun.jna.platform.win32.COM.COMUtils;
import ghidra.pty.windows.jna.ConsoleApiNative;
import ghidra.pty.windows.jna.ConsoleApiNative.COORD;
public class PseudoConsoleHandle extends Handle {
@ -40,4 +42,11 @@ public class PseudoConsoleHandle extends Handle {
protected State newState(HANDLE handle) {
return new PseudoConsoleState(handle);
}
public void resize(short rows, short cols) {
COORD.ByValue size = new COORD.ByValue();
size.X = cols;
size.Y = rows;
COMUtils.checkRC(ConsoleApiNative.INSTANCE.ResizePseudoConsole(getNative(), size));
}
}

View file

@ -18,6 +18,7 @@ package ghidra.pty.windows.jna;
import java.util.List;
import com.sun.jna.*;
import com.sun.jna.Structure.FieldOrder;
import com.sun.jna.platform.win32.WinBase;
import com.sun.jna.platform.win32.WinDef.*;
import com.sun.jna.platform.win32.WinNT.*;
@ -31,8 +32,9 @@ public interface ConsoleApiNative extends StdCallLibrary {
SECURITY_ATTRIBUTES.ByReference lpPipeAttributes, DWORD nSize);
HRESULT CreatePseudoConsole(COORD.ByValue size, HANDLE hInput, HANDLE hOutput,
DWORD dwFlags,
HANDLEByReference phPC);
DWORD dwFlags, HANDLEByReference phPC);
HRESULT ResizePseudoConsole(HANDLE hPC, COORD.ByValue size);
void ClosePseudoConsole(HANDLE hPC);
@ -85,20 +87,16 @@ public interface ConsoleApiNative extends StdCallLibrary {
HANDLEByReference phToken);
*/
public static class COORD extends Structure implements Structure.ByValue {
public static class ByReference extends COORD
implements Structure.ByReference {
@FieldOrder({ "X", "Y" })
public static class COORD extends Structure {
public static class ByValue extends COORD implements Structure.ByValue {
}
public static final List<String> FIELDS = createFieldsOrder("X", "Y");
public static class ByReference extends COORD implements Structure.ByReference {
}
public short X;
public short Y;
@Override
protected List<String> getFieldOrder() {
return FIELDS;
}
}
public static class SECURITY_ATTRIBUTES extends Structure {

View file

@ -24,6 +24,7 @@ import org.junit.Before;
import org.junit.Test;
import ghidra.app.script.AskDialog;
import ghidra.pty.Pty;
import ghidra.pty.PtyChild.Echo;
import ghidra.pty.PtySession;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
@ -77,7 +78,7 @@ public class SshPtyTest extends AbstractGhidraHeadedIntegrationTest {
@Test
public void testSessionBash() throws IOException, InterruptedException {
try (SshPty pty = factory.openpty()) {
try (Pty pty = factory.openpty()) {
PtySession bash = pty.getChild().session(new String[] { "bash" }, null);
OutputStream out = pty.getParent().getOutputStream();
out.write("exit\n".getBytes("UTF-8"));
@ -89,7 +90,7 @@ public class SshPtyTest extends AbstractGhidraHeadedIntegrationTest {
@Test
public void testDisableEcho() throws IOException, InterruptedException {
try (SshPty pty = factory.openpty()) {
try (Pty pty = factory.openpty()) {
PtySession bash =
pty.getChild().session(new String[] { "bash" }, null, Echo.OFF);
OutputStream out = pty.getParent().getOutputStream();

View file

@ -40,7 +40,7 @@ public class ConPtyTest extends AbstractPtyTest {
@Test
public void testSessionCmd() throws IOException, InterruptedException {
try (Pty pty = ConPty.openpty()) {
try (Pty pty = ConPtyFactory.INSTANCE.openpty()) {
PtySession cmd = pty.getChild().session(new String[] { DummyProc.which("cmd") }, null);
pty.getParent().getOutputStream().write("exit\r\n".getBytes());
assertEquals(0, cmd.waitExited());
@ -49,7 +49,7 @@ public class ConPtyTest extends AbstractPtyTest {
@Test
public void testSessionNonExistent() throws IOException, InterruptedException {
try (Pty pty = ConPty.openpty()) {
try (Pty pty = ConPtyFactory.INSTANCE.openpty()) {
pty.getChild().session(new String[] { "thisHadBetterNoExist" }, null);
fail();
}
@ -60,7 +60,7 @@ public class ConPtyTest extends AbstractPtyTest {
@Test
public void testSessionCmdEchoTest() throws IOException, InterruptedException {
try (Pty pty = ConPty.openpty()) {
try (Pty pty = ConPtyFactory.INSTANCE.openpty()) {
PtyParent parent = pty.getParent();
PrintWriter writer = new PrintWriter(parent.getOutputStream());
BufferedReader reader = loggingReader(parent.getInputStream());
@ -84,7 +84,7 @@ public class ConPtyTest extends AbstractPtyTest {
@Test
public void testSessionGdbLineLength() throws IOException, InterruptedException {
try (Pty pty = ConPty.openpty()) {
try (Pty pty = ConPtyFactory.INSTANCE.openpty()) {
PtyParent parent = pty.getParent();
PrintWriter writer = new PrintWriter(parent.getOutputStream());
BufferedReader reader = loggingReader(parent.getInputStream());
@ -137,7 +137,7 @@ public class ConPtyTest extends AbstractPtyTest {
@Test
public void testGdbInterruptConPty() throws Exception {
try (Pty pty = ConPty.openpty()) {
try (Pty pty = ConPtyFactory.INSTANCE.openpty()) {
PtyParent parent = pty.getParent();
PrintWriter writer = new PrintWriter(parent.getOutputStream());
//BufferedReader reader = loggingReader(parent.getInputStream());
@ -171,7 +171,7 @@ public class ConPtyTest extends AbstractPtyTest {
@Test
public void testGdbMiConPty() throws Exception {
try (Pty pty = ConPty.openpty()) {
try (Pty pty = ConPtyFactory.INSTANCE.openpty()) {
PtyParent parent = pty.getParent();
PrintWriter writer = new PrintWriter(parent.getOutputStream());
//BufferedReader reader = loggingReader(parent.getInputStream());

View file

@ -23,7 +23,9 @@ import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.*;
import org.apache.commons.lang3.StringUtils;
import org.junit.Test;
import docking.widgets.OkDialog;
@ -38,7 +40,7 @@ import ghidra.util.SystemUtilities;
public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerGUITest {
protected static byte[] ascii(String str) {
try {
return str.getBytes("US-ASCII");
return str.getBytes("UTF-8");
}
catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
@ -68,15 +70,17 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerGUITest {
PtySession session = pty.getChild().session(new String[] { "/usr/bin/bash" }, env);
PtyParent parent = pty.getParent();
try (Terminal term = terminalService.createWithStreams(Charset.forName("US-ASCII"),
try (Terminal term = terminalService.createWithStreams(Charset.forName("UTF-8"),
parent.getInputStream(), parent.getOutputStream())) {
term.addTerminalListener(new TerminalListener() {
@Override
public void resized(int cols, int rows) {
public void resized(short cols, short rows) {
System.err.println("resized: " + cols + "x" + rows);
parent.setWindowSize(cols, rows);
}
});
session.waitExited();
pty.close();
}
}
}
@ -94,18 +98,45 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerGUITest {
try (Pty pty = factory.openpty()) {
Map<String, String> env = new HashMap<>(System.getenv());
PtySession session =
pty.getChild().session(new String[] { "C:\\Windows\\cmd.exe" }, env);
pty.getChild().session(new String[] { "C:\\Windows\\system32\\cmd.exe" }, env);
PtyParent parent = pty.getParent();
try (Terminal term = terminalService.createWithStreams(Charset.forName("US-ASCII"),
try (Terminal term = terminalService.createWithStreams(Charset.forName("UTF-8"),
parent.getInputStream(), parent.getOutputStream())) {
term.addTerminalListener(new TerminalListener() {
@Override
public void resized(int cols, int rows) {
public void resized(short cols, short rows) {
System.err.println("resized: " + cols + "x" + rows);
parent.setWindowSize(cols, rows);
}
});
session.waitExited();
pty.close();
}
}
}
@Test
@SuppressWarnings("resource")
public void testCmd80x25() throws Exception {
assumeFalse(SystemUtilities.isInTestingBatchMode());
assumeTrue(OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS);
terminalService = addPlugin(tool, TerminalPlugin.class);
clipboardService = addPlugin(tool, ClipboardPlugin.class);
PtyFactory factory = PtyFactory.local();
try (Pty pty = factory.openpty(80, 25)) {
Map<String, String> env = new HashMap<>(System.getenv());
PtySession session =
pty.getChild().session(new String[] { "C:\\Windows\\system32\\cmd.exe" }, env);
PtyParent parent = pty.getParent();
try (Terminal term = terminalService.createWithStreams(Charset.forName("UTF-8"),
parent.getInputStream(), parent.getOutputStream())) {
term.setFixedSize(80, 25);
session.waitExited();
pty.close();
}
}
}
@ -123,9 +154,9 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerGUITest {
terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService
.createNullTerminal(Charset.forName("US-ASCII"), buf -> {
.createNullTerminal(Charset.forName("UTF-8"), buf -> {
})) {
term.setFixedSize(25, 80);
term.setFixedSize(80, 25);
term.injectDisplayOutput(TEST_CONTENTS);
term.provider.findDialog.txtFind.setText("term");
@ -155,9 +186,9 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerGUITest {
terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService
.createNullTerminal(Charset.forName("US-ASCII"), buf -> {
.createNullTerminal(Charset.forName("UTF-8"), buf -> {
})) {
term.setFixedSize(25, 80);
term.setFixedSize(80, 25);
term.injectDisplayOutput(TEST_CONTENTS);
term.provider.findDialog.txtFind.setText("term");
@ -184,9 +215,9 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerGUITest {
terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService
.createNullTerminal(Charset.forName("US-ASCII"), buf -> {
.createNullTerminal(Charset.forName("UTF-8"), buf -> {
})) {
term.setFixedSize(25, 80);
term.setFixedSize(80, 25);
term.injectDisplayOutput(TEST_CONTENTS);
term.provider.findDialog.txtFind.setText("term");
@ -216,9 +247,9 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerGUITest {
terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService
.createNullTerminal(Charset.forName("US-ASCII"), buf -> {
.createNullTerminal(Charset.forName("UTF-8"), buf -> {
})) {
term.setFixedSize(25, 80);
term.setFixedSize(80, 25);
term.injectDisplayOutput(TEST_CONTENTS);
term.provider.findDialog.txtFind.setText("term");
@ -245,9 +276,9 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerGUITest {
terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService
.createNullTerminal(Charset.forName("US-ASCII"), buf -> {
.createNullTerminal(Charset.forName("UTF-8"), buf -> {
})) {
term.setFixedSize(25, 80);
term.setFixedSize(80, 25);
term.injectDisplayOutput(TEST_CONTENTS);
term.provider.findDialog.txtFind.setText("o?term");
@ -283,9 +314,9 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerGUITest {
terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService
.createNullTerminal(Charset.forName("US-ASCII"), buf -> {
.createNullTerminal(Charset.forName("UTF-8"), buf -> {
})) {
term.setFixedSize(25, 80);
term.setFixedSize(80, 25);
term.injectDisplayOutput(TEST_CONTENTS);
term.provider.findDialog.txtFind.setText("term");
@ -315,9 +346,9 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerGUITest {
terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService
.createNullTerminal(Charset.forName("US-ASCII"), buf -> {
.createNullTerminal(Charset.forName("UTF-8"), buf -> {
})) {
term.setFixedSize(25, 80);
term.setFixedSize(80, 25);
term.injectDisplayOutput(TEST_CONTENTS);
term.provider.findDialog.txtFind.setText("term");
@ -347,9 +378,9 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerGUITest {
terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService
.createNullTerminal(Charset.forName("US-ASCII"), buf -> {
.createNullTerminal(Charset.forName("UTF-8"), buf -> {
})) {
term.setFixedSize(25, 80);
term.setFixedSize(80, 25);
term.injectDisplayOutput(TEST_CONTENTS);
term.provider.findDialog.txtFind.setText("o?term");
@ -378,4 +409,190 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerGUITest {
dialog.close();
}
}
protected String csi(char f, int... params) {
return "\033[" +
IntStream.of(params).mapToObj(Integer::toString).collect(Collectors.joining(";")) + f;
}
protected String title(String title) {
return "\033]0;" + title + "\007";
}
protected final static String HIDE_CURSOR = "\033[?25l";
protected final static String SHOW_CURSOR = "\033[?25h";
protected void send(Terminal term, String... parts) throws Exception {
String joined = Stream.of(parts).collect(Collectors.joining());
term.injectDisplayOutput(joined.getBytes("UTF-8"));
}
@Test
@SuppressWarnings("resource")
public void testSimulateLinuxPtyResetAndEol() throws Exception {
terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService
.createNullTerminal(Charset.forName("UTF-8"), buf -> {
})) {
term.setFixedSize(40, 25);
send(term,
csi('J', 3), csi('H'), csi('J', 2),
title(name.getMethodName()),
/**
* Linux/bash goes one character past, sends CR, repeats the character, and
* continues. No LF. The line feed occurs by virtue of the local line wrap.
*/
"12345678901234567890123456789012345678901",
"\r",
"1234567890");
assertEquals("1234567890123456789012345678901234567890", term.getLineText(0));
assertEquals("1234567890", term.getLineText(1));
assertEquals(10, term.getCursorColumn());
assertEquals(1, term.getCursorRow());
}
}
@Test
@SuppressWarnings("resource")
public void testSimulateLinuxPtyTypePastEol() throws Exception {
terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService
.createNullTerminal(Charset.forName("UTF-8"), buf -> {
})) {
term.setFixedSize(40, 25);
send(term,
title(name.getMethodName()),
/**
* Echoing characters back works similarly to sending characters. When the last
* column is filled (the application knows the terminal width) Linux/bash sends an
* extra space to induce a line wrap, then sends CR.
*/
"123456789012345678901234567890123456789", // One before the last column
"0 \r");
assertEquals("1234567890123456789012345678901234567890", term.getLineText(0));
assertEquals(" ", term.getLineText(1));
assertEquals(0, term.getCursorColumn());
assertEquals(1, term.getCursorRow());
}
}
@Test
@SuppressWarnings("resource")
public void testSimulateLinuxPtyEchoPastEol() throws Exception {
terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService
.createNullTerminal(Charset.forName("UTF-8"), buf -> {
})) {
term.setFixedSize(40, 25);
send(term,
title(name.getMethodName()),
/**
* The echo command itself pays no heed to the terminal width. Wrapping is purely
* terminal side.
*/
"1234567890123456789012345678901234567890asdfasdf\r\n");
assertEquals("1234567890123456789012345678901234567890", term.getLineText(0));
assertEquals("asdfasdf", term.getLineText(1));
assertEquals("", term.getLineText(2));
assertEquals(0, term.getCursorColumn());
assertEquals(2, term.getCursorRow());
}
}
@Test
@SuppressWarnings("resource")
public void testSimulateLinuxPtyTypePastEolLastLine() throws Exception {
terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService
.createNullTerminal(Charset.forName("UTF-8"), buf -> {
})) {
term.setFixedSize(40, 25);
send(term,
title(name.getMethodName()),
"top line\r\n",
StringUtils.repeat("\r\n", 23),
"123456789012345678901234567890123456789", // One before the last column
"0 \r");
assertEquals(1, term.getScrollBackRows());
assertEquals("top line", term.getLineText(-1));
assertEquals("1234567890123456789012345678901234567890", term.getLineText(23));
assertEquals(" ", term.getLineText(24));
assertEquals(0, term.getCursorColumn());
assertEquals(24, term.getCursorRow());
}
}
@Test
@SuppressWarnings("resource")
public void testSimulateWindowsPtyStartCmd() throws Exception {
terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService
.createNullTerminal(Charset.forName("UTF-8"), buf -> {
})) {
term.setFixedSize(80, 25);
send(term,
csi('J', 2), HIDE_CURSOR, csi('m'), csi('H'),
StringUtils.repeat("\r\n", 32), // for a 25-line terminal? No matter.
csi('H'),
title("C:\\Windows\\system32\\cmd.exe\0"),
SHOW_CURSOR, HIDE_CURSOR,
// Line 1: Length is 43
"Microsoft Windows [Version XXXXXXXXXXXXXXX]",
csi('X', 37), csi('C', 37), "\r\n", // 37 + 43 = 80
// Line 2: Length is 52
"(c) 20XX Microsoft Corporation. All rights reserved.",
csi('X', 28), csi('C', 28), "\r\n", // 28 + 52 = 80
// Line 3: Blank
csi('X', 80), csi('C', 80), "\r\n", // 80 + 0 = 80
// Line 4: No X or C sequences. Probably because shorter to put literal " "
"C:\\XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX> ");
send(term, " ");
send(term, "\r\n");
send(term,
csi('X', 80), csi('C', 80), "\r\n",
csi('X', 80), csi('C', 80), "\r\n",
csi('X', 80), csi('C', 80), "\r\n",
csi('X', 80), csi('C', 80), "\r\n",
csi('X', 80), csi('C', 80), "\r\n",
csi('X', 80), csi('C', 80), "\r\n",
csi('X', 80), csi('C', 80), "\r\n",
csi('X', 80), csi('C', 80), "\r\n",
csi('X', 80), csi('C', 80), "\r\n",
csi('X', 80), csi('C', 80), "\r\n",
csi('X', 80), csi('C', 80), "\r\n",
csi('X', 80), csi('C', 80), "\r\n",
csi('X', 80), csi('C', 80), "\r\n",
csi('X', 80), csi('C', 80), "\r\n",
csi('X', 80), csi('C', 80), "\r\n",
csi('X', 80), csi('C', 80), "\r\n",
csi('X', 80), csi('C', 80), "\r\n",
csi('X', 80), csi('C', 80), "\r\n",
csi('X', 80), csi('C', 80), "\r\n",
csi('X', 80), csi('C', 80), "\r\n",
csi('X', 80), csi('C', 80),
csi('H', 4, 79),
SHOW_CURSOR);
assertEquals(
"C:\\XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX> ",
term.getLineText(3));
assertEquals(78, term.getCursorColumn());
assertEquals(3, term.getCursorRow());
}
}
}