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

View file

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

View file

@ -21,8 +21,6 @@ import java.util.concurrent.CompletableFuture;
import org.junit.Ignore; import org.junit.Ignore;
import agent.gdb.manager.GdbManager; import agent.gdb.manager.GdbManager;
import ghidra.pty.PtyFactory;
import ghidra.pty.windows.ConPtyFactory;
@Ignore("Need compatible version on CI") @Ignore("Need compatible version on CI")
public class SpawnedWindowsMi2GdbManagerTest extends AbstractGdbManagerTest { public class SpawnedWindowsMi2GdbManagerTest extends AbstractGdbManagerTest {
@ -36,10 +34,4 @@ public class SpawnedWindowsMi2GdbManagerTest extends AbstractGdbManagerTest {
throw new AssertionError(e); 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 { protected void displayInTerminal(PtyParent parent, Runnable waiter) throws PluginException {
TerminalService terminalService = ensureTerminalService(); 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())) { parent.getInputStream(), parent.getOutputStream())) {
term.addTerminalListener(new TerminalListener() { term.addTerminalListener(new TerminalListener() {
@Override @Override
public void resized(int cols, int rows) { public void resized(short cols, short rows) {
parent.setWindowSize(cols, rows); parent.setWindowSize(cols, rows);
} }
}); });

View file

@ -56,12 +56,62 @@ public class DefaultTerminal implements Terminal {
} }
@Override @Override
public void setFixedSize(int rows, int cols) { public void setFixedSize(short cols, short rows) {
provider.setFixedSize(rows, cols); provider.setFixedSize(cols, rows);
} }
@Override @Override
public void setDynamicSize() { public void setDynamicSize() {
provider.setDyanmicSize(); 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.CharBuffer;
import java.nio.charset.*; import java.nio.charset.*;
import ghidra.app.plugin.core.terminal.vt.VtHandler.KeyMode;
import ghidra.util.Msg; import ghidra.util.Msg;
/** /**
@ -45,24 +46,32 @@ public abstract class TerminalAwtEventEncoder {
} }
public static final byte ESC = (byte) 0x1b; 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_INSERT = vtseq(2);
public static final byte[] CODE_DELETE = vtseq(3); public static final byte[] CODE_DELETE = vtseq(3);
public static final byte[] CODE_HOME = { ESC, '[', 'H' }; // Believe it or not, \r is ENTER on both Windows and Linux!
public static final byte[] CODE_END = { ESC, '[', 'F' }; public static final byte[] CODE_ENTER = { '\r' };
public static final byte[] CODE_PAGE_UP = vtseq(5); public static final byte[] CODE_PAGE_UP = vtseq(5);
public static final byte[] CODE_PAGE_DOWN = vtseq(6); public static final byte[] CODE_PAGE_DOWN = vtseq(6);
public static final byte[] CODE_NUMPAD5 = { ESC, '[', 'E' }; public static final byte[] CODE_NUMPAD5 = { ESC, '[', 'E' };
public static final byte[] CODE_UP = { ESC, FUNC, 'A' }; public static final byte[] CODE_UP_NORMAL = { ESC, '[', 'A' };
public static final byte[] CODE_DOWN = { ESC, FUNC, 'B' }; public static final byte[] CODE_DOWN_NORMAL = { ESC, '[', 'B' };
public static final byte[] CODE_RIGHT = { ESC, FUNC, 'C' }; public static final byte[] CODE_RIGHT_NORMAL = { ESC, '[', 'C' };
public static final byte[] CODE_LEFT = { ESC, FUNC, 'D' }; public static final byte[] CODE_LEFT_NORMAL = { ESC, '[', 'D' };
public static final byte[] CODE_F1 = { ESC, FUNC, 'P' }; public static final byte[] CODE_UP_APPLICATION = { ESC, 'O', 'A' };
public static final byte[] CODE_F2 = { ESC, FUNC, 'Q' }; public static final byte[] CODE_DOWN_APPLICATION = { ESC, 'O', 'B' };
public static final byte[] CODE_F3 = { ESC, FUNC, 'R' }; public static final byte[] CODE_RIGHT_APPLICATION = { ESC, 'O', 'C' };
public static final byte[] CODE_F4 = { ESC, FUNC, 'S' }; 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_F5 = vtseq(15);
public static final byte[] CODE_F6 = vtseq(17); public static final byte[] CODE_F6 = vtseq(17);
public static final byte[] CODE_F7 = vtseq(18); 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) { if (e.getModifiersEx() != 0) {
return getModifiedAnsiKeyCode(e); return getModifiedAnsiKeyCode(e);
} }
return switch (e.getKeyCode()) { return switch (e.getKeyCode()) {
case KeyEvent.VK_INSERT -> CODE_INSERT; case KeyEvent.VK_INSERT -> CODE_INSERT;
// NB. CODE_DELETE is handled in keyTyped // NB. CODE_DELETE is handled in keyTyped
case KeyEvent.VK_HOME -> CODE_HOME; // Yes, HOME and END are considered CURSOR keys
case KeyEvent.VK_END -> CODE_END; 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_UP -> CODE_PAGE_UP;
case KeyEvent.VK_PAGE_DOWN -> CODE_PAGE_DOWN; case KeyEvent.VK_PAGE_DOWN -> CODE_PAGE_DOWN;
case KeyEvent.VK_NUMPAD5 -> CODE_NUMPAD5; case KeyEvent.VK_NUMPAD5 -> CODE_NUMPAD5;
case KeyEvent.VK_UP -> CODE_UP; case KeyEvent.VK_UP -> cursorMode.choose(CODE_UP_NORMAL, CODE_UP_APPLICATION);
case KeyEvent.VK_DOWN -> CODE_DOWN; case KeyEvent.VK_DOWN -> cursorMode.choose(CODE_DOWN_NORMAL, CODE_DOWN_APPLICATION);
case KeyEvent.VK_RIGHT -> CODE_RIGHT; case KeyEvent.VK_RIGHT -> cursorMode.choose(CODE_RIGHT_NORMAL, CODE_RIGHT_APPLICATION);
case KeyEvent.VK_LEFT -> CODE_LEFT; case KeyEvent.VK_LEFT -> cursorMode.choose(CODE_LEFT_NORMAL, CODE_LEFT_APPLICATION);
case KeyEvent.VK_F1 -> CODE_F1; case KeyEvent.VK_F1 -> CODE_F1;
case KeyEvent.VK_F2 -> CODE_F2; case KeyEvent.VK_F2 -> CODE_F2;
case KeyEvent.VK_F3 -> CODE_F3; case KeyEvent.VK_F3 -> CODE_F3;
@ -196,8 +206,8 @@ public abstract class TerminalAwtEventEncoder {
}; };
} }
public void keyPressed(KeyEvent e) { public void keyPressed(KeyEvent e, KeyMode cursorKeyMode, KeyMode keypadMode) {
byte[] bytes = getAnsiKeyCode(e); byte[] bytes = getAnsiKeyCode(e, cursorKeyMode, keypadMode);
bb.put(bytes); bb.put(bytes);
generateBytesExc(); generateBytesExc();
} }
@ -278,6 +288,10 @@ public abstract class TerminalAwtEventEncoder {
public void sendChar(char c) { public void sendChar(char c) {
switch (c) { switch (c) {
case 0x0a:
bb.put(CODE_ENTER);
generateBytesExc();
break;
case 0x7f: case 0x7f:
bb.put(CODE_DELETE); bb.put(CODE_DELETE);
generateBytesExc(); generateBytesExc();
@ -296,7 +310,9 @@ public abstract class TerminalAwtEventEncoder {
protected void generateBytesExc() { protected void generateBytesExc() {
bb.flip(); bb.flip();
try { try {
generateBytes(bb); if (bb.hasRemaining()) {
generateBytes(bb);
}
} }
catch (Throwable t) { catch (Throwable t) {
Msg.error(this, "Error generating bytes: " + t, 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; protected VtBuffer buffer = bufPrimary;
// Flags for what's been enabled // Flags for what's been enabled
protected boolean showCursor;
protected boolean bracketedPaste; protected boolean bracketedPaste;
protected boolean reportMousePress; protected boolean reportMousePress;
protected boolean reportMouseRelease; protected boolean reportMouseRelease;
protected boolean reportFocus; protected boolean reportFocus;
protected KeyMode cursorKeyMode = KeyMode.NORMAL;
protected KeyMode keypadMode = KeyMode.NORMAL;
private Object lock = new Object(); private Object lock = new Object();
@ -133,6 +136,8 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler {
reportMousePress = false; reportMousePress = false;
reportMouseRelease = false; reportMouseRelease = false;
reportFocus = false; reportFocus = false;
cursorKeyMode = KeyMode.NORMAL;
keypadMode = KeyMode.NORMAL;
} }
public void processInput(ByteBuffer buffer) { public void processInput(ByteBuffer buffer) {
@ -275,7 +280,7 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler {
try { try {
// A little strange using both unicode and vt charsets.... // A little strange using both unicode and vt charsets....
buffer.putChar(curVtCharset.mapChar(cb.get())); buffer.putChar(curVtCharset.mapChar(cb.get()));
buffer.moveCursorRight(1); buffer.moveCursorRight(1, true, showCursor);
} }
catch (Throwable t) { catch (Throwable t) {
Msg.error(this, "Error handling character: " + t, t); Msg.error(this, "Error handling character: " + t, t);
@ -291,7 +296,7 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler {
@Override @Override
public void handleBackSpace() { public void handleBackSpace() {
buffer.moveCursorLeft(1); buffer.moveCursorLeft(1, true);
} }
@Override @Override
@ -308,7 +313,7 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler {
@Override @Override
public void handleLineFeed() { public void handleLineFeed() {
buffer.moveCursorDown(1); buffer.moveCursorDown(1, true);
} }
@Override @Override
@ -392,15 +397,17 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler {
} }
@Override @Override
public void handleApplicationCursorKeys(boolean en) { public void handleCursorKeyMode(KeyMode mode) {
// Not sure what this means. Ignore for now. this.cursorKeyMode = mode;
Msg.trace(this, "TODO: handleApplicationCursorKeys: " + en);
} }
@Override @Override
public void handleApplicationKeypad(boolean en) { public void handleKeypadMode(KeyMode mode) {
// Not sure what this means. Ignore for now. /**
Msg.trace(this, "TODO: handleApplicationKeypad: " + en); * 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 @Override
@ -417,6 +424,11 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler {
@Override @Override
public void handleShowCursor(boolean show) { public void handleShowCursor(boolean show) {
this.showCursor = show;
if (show) {
bufPrimary.checkVerticalScroll();
bufAlternate.checkVerticalScroll();
}
panel.fieldPanel.setCursorOn(show); panel.fieldPanel.setCursorOn(show);
} }
@ -470,13 +482,13 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler {
buffer.moveCursorUp(n); buffer.moveCursorUp(n);
return; return;
case DOWN: case DOWN:
buffer.moveCursorDown(n); buffer.moveCursorDown(n, false);
return; return;
case FORWARD: case FORWARD:
buffer.moveCursorRight(n); buffer.moveCursorRight(n, false, showCursor);
return; return;
case BACK: case BACK:
buffer.moveCursorLeft(n); buffer.moveCursorLeft(n, false);
return; return;
} }
} }
@ -603,6 +615,10 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler {
return buffer.getCurX(); return buffer.getCurX();
} }
public int resetCursorBottom() {
return buffer.resetBottomY();
}
public int getCols() { public int getCols() {
return buffer.getCols(); return buffer.getCols();
} }
@ -630,4 +646,8 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler {
layoutCache.clear(); layoutCache.clear();
buildLayouts(); 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 cols the number of columns
* @param rows the number of rows * @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)) { if (provider.isLocalActionKeyBinding(e)) {
return; // Do not consume, so action can take it return; // Do not consume, so action can take it
} }
eventEncoder.keyPressed(e); eventEncoder.keyPressed(e, model.cursorKeyMode, model.keypadMode);
e.consume(); e.consume();
} }
@ -324,7 +324,7 @@ public class TerminalPanel extends JPanel implements FieldLocationListener, Fiel
terminalListeners.remove(listener); terminalListeners.remove(listener);
} }
protected void notifyTerminalResized(int cols, int rows) { protected void notifyTerminalResized(short cols, short rows) {
for (TerminalListener l : terminalListeners) { for (TerminalListener l : terminalListeners) {
try { try {
l.resized(cols, rows); 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, fieldPanel.setCursorPosition(BigInteger.valueOf(model.getCursorRow() + scrollBack), 0, 0,
model.getCursorColumn()); model.getCursorColumn());
if (scroll) { 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() { protected void resizeTerminalToWindow() {
Rectangle bounds = scroller.getViewportBorderBounds(); Rectangle bounds = scroller.getViewportBorderBounds();
int rows = bounds.height / metrics.getHeight();
int cols = bounds.width / metrics.charWidth('M'); 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) { protected void resizeTerminal(short cols, short rows) {
if (model.resizeTerminal(cols, rows)) { if (model.resizeTerminal(Short.toUnsignedInt(cols), Short.toUnsignedInt(rows))) {
notifyTerminalResized(model.getCols(), model.getRows()); 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, * needed. If the terminal size changes as a result of this call,
* {@link TerminalListener#resized(int, int)} is invoked. * {@link TerminalListener#resized(int, int)} is invoked.
* *
* @param rows the number of rows
* @param cols the number of columns * @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; this.fixedSize = true;
scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_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); scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
resizeTerminalToWindow(); 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.*;
import ghidra.framework.plugintool.util.PluginStatus; import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.util.Msg; import ghidra.util.Msg;
import ghidra.util.Swing;
/** /**
* The plugin that provides {@link TerminalService} * The plugin that provides {@link TerminalService}
@ -52,13 +53,15 @@ public class TerminalPlugin extends Plugin implements TerminalService {
} }
public TerminalProvider createProvider(Charset charset, VtOutput outputCb) { public TerminalProvider createProvider(Charset charset, VtOutput outputCb) {
TerminalProvider provider = new TerminalProvider(this, charset); return Swing.runNow(() -> {
provider.setOutputCallback(outputCb); TerminalProvider provider = new TerminalProvider(this, charset);
provider.addToTool(); provider.setOutputCallback(outputCb);
provider.setVisible(true); provider.addToTool();
providers.add(provider); provider.setVisible(true);
provider.setClipboardService(clipboardService); providers.add(provider);
return provider; provider.setClipboardService(clipboardService);
return provider;
});
} }
@Override @Override
@ -72,6 +75,7 @@ public class TerminalPlugin extends Plugin implements TerminalService {
return new ThreadedTerminal(createProvider(charset, buf -> { return new ThreadedTerminal(createProvider(charset, buf -> {
while (buf.hasRemaining()) { while (buf.hasRemaining()) {
try { try {
//ThreadedTerminal.printBuffer(">> ", buf);
channel.write(buf); channel.write(buf);
} }
catch (IOException e) { catch (IOException e) {

View file

@ -299,11 +299,42 @@ public class TerminalProvider extends ComponentProviderAdapter {
return false; return false;
} }
public void setFixedSize(int rows, int cols) { public void setFixedSize(short cols, short rows) {
panel.setFixedTerminalSize(rows, cols); panel.setFixedTerminalSize(cols, rows);
} }
public void setDyanmicSize() { public void setDyanmicSize() {
panel.setDynamicTerminalSize(); 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); g.setColor(cursorColor);
int x = startX + findX(cursorLoc.col()); int x = startX + findX(cursorLoc.col());
g.drawRect(x, -getHeightAbove(), em - 1, getHeight() - 1); 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(); super.close();
} }
@SuppressWarnings("unused") // diagnostic static void printBuffer(String prefix, ByteBuffer bb) {
private void printBuffer() { byte[] bytes = new byte[bb.remaining()];
byte[] bytes = new byte[buffer.remaining()]; bb.get(bb.position(), bytes);
buffer.get(buffer.position(), bytes); System.err.print(prefix);
//System.err.println("<< " + NumericUtilities.convertBytesToString(bytes, ":"));
try { try {
String str = new String(bytes, "US-ASCII"); String str = new String(bytes, "US-ASCII");
for (char c : str.toCharArray()) { for (char c : str.toCharArray()) {
@ -93,7 +92,7 @@ public class ThreadedTerminal extends DefaultTerminal {
return; return;
} }
buffer.flip(); buffer.flip();
//printBuffer(); //printBuffer("<< ", buffer);
synchronized (buffer) { synchronized (buffer) {
provider.processInput(buffer); provider.processInput(buffer);
} }

View file

@ -41,6 +41,7 @@ public class VtBuffer {
protected int curY; protected int curY;
protected int savedX; protected int savedX;
protected int savedY; protected int savedY;
protected int bottomY; // for scrolling UI after an update
protected int scrollStart; protected int scrollStart;
protected int scrollEnd; // exclusive protected int scrollEnd; // exclusive
@ -128,6 +129,8 @@ public class VtBuffer {
if (c == 0) { if (c == 0) {
return; return;
} }
checkVerticalScroll();
// At this point, we have no choice but to wrap
lines.get(curY).putChar(curX, c, curAttrs); lines.get(curY).putChar(curX, c, curAttrs);
} }
@ -136,7 +139,7 @@ public class VtBuffer {
*/ */
public void tab() { public void tab() {
int n = TAB_WIDTH + (-curX % TAB_WIDTH); int n = TAB_WIDTH + (-curX % TAB_WIDTH);
moveCursorRight(n); moveCursorRight(n, false, false);
} }
/** /**
@ -147,7 +150,7 @@ public class VtBuffer {
return; return;
} }
int n = (curX - 1) % TAB_WIDTH + 1; 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. * This does <em>not</em> move the cursor down.
*/ */
public void carriageReturn() { public void carriageReturn() {
if (curX == 0) {
return;
}
int prevY = curY - 1;
if (prevY >= 0 && prevY < lines.size()) {
lines.get(prevY).wrappedToNext = false;
}
curX = 0; 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 * 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)}. * 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) { public void moveCursorDown(int n, boolean dedupWrap) {
curY += n; int prevY = curY - 1;
checkVerticalScroll(); 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 * Move the cursor left (backward) n columns
* *
* <p> * <p>
* If the cursor would move left of the display, it is instead moved to the far right of the * The cursor is clamped into the display. If wrap is specified, the cursor would exceed the
* previous row, unless the cursor is already on the top row, in which case, it will be placed * left side of the display, and the previous line was wrapped onto the current line, then the
* in the top-left corner of the display. NOTE: If the cursor is moved to the previous row, no * cursor will instead be moved to the end of the previous line. (It doesn't matter how far the
* heed is given to "leftovers." It doesn't matter how far to the left the cursor would have * cursor would exceed the left; it moves up at most one line.) The value of n must be positive,
* been; it is moved to the far right column and exactly one row up. The value of n must be * otherwise behavior is undefined. To move the cursor right, use {@link #moveCursorRight(int)}.
* positive, otherwise behavior is undefined. To move the cursor right, use
* {@link #moveCursorRight(int)}.
* *
* @param n the number of columns * @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) { public void moveCursorLeft(int n, boolean wrap) {
if (curX - n >= 0) { int prevY = curY - 1;
curX -= n; if (wrap && curX - n < 0 && prevY >= 0 && prevY < lines.size() &&
} lines.get(prevY).wrappedToNext) {
else if (curY > 0) {
curX = cols - 1; curX = cols - 1;
curY--; curY--;
lines.get(curY).wrappedToNext = false;
} }
curX = Math.max(0, Math.min(curX - n, cols - 1));
} }
/** /**
* Move the cursor right (forward) n columns * Move the cursor right (forward) n columns
* *
* <p> * <p>
* If the cursor would move right of the display, it is instead moved to the far left of the * The cursor is clamped into the display. If wrap is specified and the cursor would exceed the
* next row. If the cursor is already on the bottom row, the viewport is scrolled down a line. * right side of the display, the cursor will instead be wrapped to the start of the next line,
* NOTE: If the cursor is moved to the next row, no heed is given to "leftovers." It doesn't * possibly scrolling the viewport down. (It doesn't matter how far the cursor exceeds the
* matter how far to the right the cursor would have been; it is moved to the far left column * right; the cursor moves down exactly one line.) The value of n must be positive, otherwise
* and exactly one row down. The value of n must be positive, otherwise behavior is undefined. * behavior is undefined. To move the cursor left, use {@link #moveCursorLeft(int)}.
* To move the cursor left, use {@link #moveCursorLeft(int)}.
* *
* @param n the number of columns * @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) { public void moveCursorRight(int n, boolean wrap, boolean isCursorShowing) {
curX += n; if (wrap && curX + n >= cols) {
if (curX >= cols) { checkVerticalScroll();
curX = 0; curX = 0;
lines.get(curY).wrappedToNext = true;
curY++; 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() { public void restoreCursorPos() {
curX = savedX; curX = savedX;
curY = savedY; curY = savedY;
bottomY = Math.max(bottomY, curY);
} }
/** /**
@ -326,8 +362,9 @@ public class VtBuffer {
* @param col the desired column, 0 up, left to right * @param col the desired column, 0 up, left to right
*/ */
public void moveCursor(int row, int col) { public void moveCursor(int row, int col) {
this.curX = Math.max(0, Math.min(cols - 1, col)); curX = Math.max(0, Math.min(cols - 1, col));
this.curY = Math.max(0, Math.min(rows - 1, row)); 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) { public void erase(Erasure erasure) {
switch (erasure) { switch (erasure) {
case TO_DISPLAY_END: case TO_DISPLAY_END:
if (curY >= lines.size()) {
return;
}
for (int y = curY; y < rows; y++) { for (int y = curY; y < rows; y++) {
VtLine line = lines.get(y); VtLine line = lines.get(y);
if (y == curY) { if (y == curY) {
@ -403,12 +443,21 @@ public class VtBuffer {
scrollBack.clear(); scrollBack.clear();
return; return;
case TO_LINE_END: case TO_LINE_END:
if (curY >= lines.size()) {
return;
}
lines.get(curY).clearToEnd(curX); lines.get(curY).clearToEnd(curX);
return; return;
case TO_LINE_START: case TO_LINE_START:
if (curY >= lines.size()) {
return;
}
lines.get(curY).clearToStart(curX, curAttrs); lines.get(curY).clearToStart(curX, curAttrs);
return; return;
case FULL_LINE: case FULL_LINE:
if (curY >= lines.size()) {
return;
}
lines.get(curY).clear(); lines.get(curY).clear();
return; return;
} }
@ -462,6 +511,9 @@ public class VtBuffer {
* @param n the number of blanks to insert. * @param n the number of blanks to insert.
*/ */
public void insertChars(int n) { public void insertChars(int n) {
if (curY >= lines.size()) {
return;
}
lines.get(curY).insert(curX, n); lines.get(curY).insert(curX, n);
} }
@ -475,6 +527,9 @@ public class VtBuffer {
* @param n the number of characters to delete * @param n the number of characters to delete
*/ */
public void deleteChars(int n) { public void deleteChars(int n) {
if (curY >= lines.size()) {
return;
}
lines.get(curY).delete(curX, curX + n); lines.get(curY).delete(curX, curX + n);
} }
@ -488,6 +543,9 @@ public class VtBuffer {
* @param n the number of characters to erase * @param n the number of characters to erase
*/ */
public void eraseChars(int n) { public void eraseChars(int n) {
if (curY >= lines.size()) {
return;
}
lines.get(curY).erase(curX, curX + n, curAttrs); lines.get(curY).erase(curX, curX + n, curAttrs);
} }
@ -750,4 +808,10 @@ public class VtBuffer {
} }
return buf.toString(); 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[] Q7 = ascii("?7");
public static final byte[] Q12 = ascii("?12"); public static final byte[] Q12 = ascii("?12");
public static final byte[] Q25 = ascii("?25"); 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[] Q1000 = ascii("?1000");
public static final byte[] Q1004 = ascii("?1004"); public static final byte[] Q1004 = ascii("?1004");
public static final byte[] Q1034 = ascii("?1034"); 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 * Check if the given buffer's contents are equal to that of the given array
* *
@ -663,7 +687,7 @@ public interface VtHandler {
handleInsertMode(en); handleInsertMode(en);
} }
else if (bufEq(csiParam, Q1)) { else if (bufEq(csiParam, Q1)) {
handleApplicationCursorKeys(en); handleCursorKeyMode(en ? KeyMode.APPLICATION : KeyMode.NORMAL);
} }
else if (bufEq(csiParam, Q7)) { else if (bufEq(csiParam, Q7)) {
handleAutoWrapMode(en); handleAutoWrapMode(en);
@ -674,6 +698,10 @@ public interface VtHandler {
else if (bufEq(csiParam, Q25)) { else if (bufEq(csiParam, Q25)) {
handleShowCursor(en); handleShowCursor(en);
} }
else if (bufEq(csiParam, Q47)) {
// NB. Same as 1047?
handleAltScreenBuffer(en, false);
}
else if (bufEq(csiParam, Q1000)) { else if (bufEq(csiParam, Q1000)) {
handleReportMouseEvents(en, en); handleReportMouseEvents(en, en);
} }
@ -684,6 +712,7 @@ public interface VtHandler {
handleMetaKey(en); handleMetaKey(en);
} }
else if (bufEq(csiParam, Q1047)) { else if (bufEq(csiParam, Q1047)) {
// NB. Same as 47?
handleAltScreenBuffer(en, false); handleAltScreenBuffer(en, false);
} }
else if (bufEq(csiParam, Q1048)) { 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. // TODO: 104;c;c;c... is color reset. I've not implemented setting them, though.
// No c given = reset all // 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 * Handle an OSC sequence
* *
@ -994,7 +1028,7 @@ public interface VtHandler {
matcher = PAT_OSC_WINDOW_TITLE.matcher(paramStr); matcher = PAT_OSC_WINDOW_TITLE.matcher(paramStr);
if (matcher.matches()) { if (matcher.matches()) {
handleWindowTitle(matcher.group("title")); handleWindowTitle(truncateAtNull(matcher.group("title")));
return; return;
} }
@ -1349,18 +1383,18 @@ public interface VtHandler {
void handleInsertMode(boolean en); 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 * Toggle auto-wrap mode

View file

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

View file

@ -16,6 +16,7 @@
package ghidra.app.plugin.core.terminal.vt; package ghidra.app.plugin.core.terminal.vt;
import ghidra.app.plugin.core.terminal.vt.VtCharset.G; import ghidra.app.plugin.core.terminal.vt.VtCharset.G;
import ghidra.app.plugin.core.terminal.vt.VtHandler.KeyMode;
public enum VtState { public enum VtState {
/** /**
@ -61,11 +62,10 @@ public enum VtState {
case ']': case ']':
return OSC_PARAM; return OSC_PARAM;
case '=': case '=':
handler.handleApplicationKeypad(true); handler.handleKeypadMode(KeyMode.APPLICATION);
return CHAR; return CHAR;
case '>': case '>':
// Normal keypad handler.handleKeypadMode(KeyMode.NORMAL);
handler.handleApplicationKeypad(false);
return CHAR; return CHAR;
case 'D': case 'D':
handler.handleScrollViewportDown(1, true); handler.handleScrollViewportDown(1, true);
@ -267,7 +267,8 @@ public enum VtState {
OSC_PARAM { OSC_PARAM {
@Override @Override
protected VtState handleNext(byte b, VtParser parser, VtHandler handler) { 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); parser.oscParam.put(b);
return OSC_PARAM; 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 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(); 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 @Override
void close(); void close();
} }

View file

@ -26,6 +26,8 @@ import ghidra.pty.windows.ConPtyFactory;
* A mechanism for opening pseudo-terminals * A mechanism for opening pseudo-terminals
*/ */
public interface PtyFactory { public interface PtyFactory {
short DEFAULT_COLS = 80;
short DEFAULT_ROWS = 25;
/** /**
* Choose a factory of local pty's for the host operating system * Choose a factory of local pty's for the host operating system
@ -35,11 +37,11 @@ public interface PtyFactory {
static PtyFactory local() { static PtyFactory local() {
switch (OperatingSystem.CURRENT_OPERATING_SYSTEM) { switch (OperatingSystem.CURRENT_OPERATING_SYSTEM) {
case MAC_OS_X: case MAC_OS_X:
return new MacosPtyFactory(); return MacosPtyFactory.INSTANCE;
case LINUX: case LINUX:
return new LinuxPtyFactory(); return LinuxPtyFactory.INSTANCE;
case WINDOWS: case WINDOWS:
return new ConPtyFactory(); return ConPtyFactory.INSTANCE;
default: default:
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@ -48,10 +50,40 @@ public interface PtyFactory {
/** /**
* Open a new pseudo-terminal * 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 * @return new new Pty
* @throws IOException for an I/O error, including cancellation * @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(); String getDescription();
} }

View file

@ -19,5 +19,11 @@ package ghidra.pty;
* The parent (UNIX "master") end of a pseudo-terminal * The parent (UNIX "master") end of a pseudo-terminal
*/ */
public interface PtyParent extends PtyEndpoint { 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.Pty;
import ghidra.pty.PtyFactory; import ghidra.pty.PtyFactory;
public class LinuxPtyFactory implements PtyFactory { public enum LinuxPtyFactory implements PtyFactory {
INSTANCE;
@Override @Override
public Pty openpty() throws IOException { public Pty openpty(short cols, short rows) throws IOException {
return LinuxPty.openpty(); LinuxPty pty = LinuxPty.openpty();
if (cols != 0 && rows != 0) {
pty.getParent().setWindowSize(cols, rows);
}
return pty;
} }
@Override @Override

View file

@ -24,14 +24,10 @@ public class LinuxPtyParent extends LinuxPtyEndpoint implements PtyParent {
} }
@Override @Override
public void setWindowSize(int cols, int rows) { public void setWindowSize(short cols, short rows) {
if (cols > 0xffff || rows > 0xffff) {
throw new IllegalArgumentException(
"Dimensions limited to unsigned shorts. Got cols=" + cols + ",rows=" + rows);
}
Winsize.ByReference ws = new Winsize.ByReference(); Winsize.ByReference ws = new Winsize.ByReference();
ws.ws_col = (short) cols; ws.ws_col = cols;
ws.ws_row = (short) rows; ws.ws_row = rows;
ws.write(); ws.write();
PosixC.INSTANCE.ioctl(fd, Winsize.TIOCSWINSZ, ws.getPointer()); 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.PtyFactory;
import ghidra.pty.linux.LinuxPty; import ghidra.pty.linux.LinuxPty;
public class MacosPtyFactory implements PtyFactory { public enum MacosPtyFactory implements PtyFactory {
INSTANCE;
@Override @Override
public Pty openpty() throws IOException { public Pty openpty(short cols, short rows) throws IOException {
return LinuxPty.openpty(); LinuxPty pty = LinuxPty.openpty();
if (cols != 0 && rows != 0) {
pty.getParent().setWindowSize(cols, rows);
}
return pty;
} }
@Override @Override

View file

@ -227,12 +227,16 @@ public class GhidraSshPtyFactory implements PtyFactory {
} }
@Override @Override
public SshPty openpty() throws IOException { public SshPty openpty(short cols, short rows) throws IOException {
if (session == null) { if (session == null) {
session = connectAndAuthenticate(); session = connectAndAuthenticate();
} }
try { 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) { catch (JSchException e) {
throw new IOException("SSH connection error", e); throw new IOException("SSH connection error", e);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -16,8 +16,10 @@
package ghidra.pty.windows; package ghidra.pty.windows;
import com.sun.jna.platform.win32.WinNT.HANDLE; 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;
import ghidra.pty.windows.jna.ConsoleApiNative.COORD;
public class PseudoConsoleHandle extends Handle { public class PseudoConsoleHandle extends Handle {
@ -40,4 +42,11 @@ public class PseudoConsoleHandle extends Handle {
protected State newState(HANDLE handle) { protected State newState(HANDLE handle) {
return new PseudoConsoleState(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 java.util.List;
import com.sun.jna.*; import com.sun.jna.*;
import com.sun.jna.Structure.FieldOrder;
import com.sun.jna.platform.win32.WinBase; import com.sun.jna.platform.win32.WinBase;
import com.sun.jna.platform.win32.WinDef.*; import com.sun.jna.platform.win32.WinDef.*;
import com.sun.jna.platform.win32.WinNT.*; import com.sun.jna.platform.win32.WinNT.*;
@ -31,8 +32,9 @@ public interface ConsoleApiNative extends StdCallLibrary {
SECURITY_ATTRIBUTES.ByReference lpPipeAttributes, DWORD nSize); SECURITY_ATTRIBUTES.ByReference lpPipeAttributes, DWORD nSize);
HRESULT CreatePseudoConsole(COORD.ByValue size, HANDLE hInput, HANDLE hOutput, HRESULT CreatePseudoConsole(COORD.ByValue size, HANDLE hInput, HANDLE hOutput,
DWORD dwFlags, DWORD dwFlags, HANDLEByReference phPC);
HANDLEByReference phPC);
HRESULT ResizePseudoConsole(HANDLE hPC, COORD.ByValue size);
void ClosePseudoConsole(HANDLE hPC); void ClosePseudoConsole(HANDLE hPC);
@ -85,20 +87,16 @@ public interface ConsoleApiNative extends StdCallLibrary {
HANDLEByReference phToken); HANDLEByReference phToken);
*/ */
public static class COORD extends Structure implements Structure.ByValue { @FieldOrder({ "X", "Y" })
public static class ByReference extends COORD public static class COORD extends Structure {
implements Structure.ByReference { 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 X;
public short Y; public short Y;
@Override
protected List<String> getFieldOrder() {
return FIELDS;
}
} }
public static class SECURITY_ATTRIBUTES extends Structure { public static class SECURITY_ATTRIBUTES extends Structure {

View file

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

View file

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

View file

@ -23,7 +23,9 @@ import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.stream.*;
import org.apache.commons.lang3.StringUtils;
import org.junit.Test; import org.junit.Test;
import docking.widgets.OkDialog; import docking.widgets.OkDialog;
@ -38,7 +40,7 @@ import ghidra.util.SystemUtilities;
public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerGUITest { public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerGUITest {
protected static byte[] ascii(String str) { protected static byte[] ascii(String str) {
try { try {
return str.getBytes("US-ASCII"); return str.getBytes("UTF-8");
} }
catch (UnsupportedEncodingException e) { catch (UnsupportedEncodingException e) {
throw new AssertionError(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); PtySession session = pty.getChild().session(new String[] { "/usr/bin/bash" }, env);
PtyParent parent = pty.getParent(); 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())) { parent.getInputStream(), parent.getOutputStream())) {
term.addTerminalListener(new TerminalListener() { term.addTerminalListener(new TerminalListener() {
@Override @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); parent.setWindowSize(cols, rows);
} }
}); });
session.waitExited(); session.waitExited();
pty.close();
} }
} }
} }
@ -94,18 +98,45 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerGUITest {
try (Pty pty = factory.openpty()) { try (Pty pty = factory.openpty()) {
Map<String, String> env = new HashMap<>(System.getenv()); Map<String, String> env = new HashMap<>(System.getenv());
PtySession session = 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(); 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())) { parent.getInputStream(), parent.getOutputStream())) {
term.addTerminalListener(new TerminalListener() { term.addTerminalListener(new TerminalListener() {
@Override @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); parent.setWindowSize(cols, rows);
} }
}); });
session.waitExited(); 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); terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService 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.injectDisplayOutput(TEST_CONTENTS);
term.provider.findDialog.txtFind.setText("term"); term.provider.findDialog.txtFind.setText("term");
@ -155,9 +186,9 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerGUITest {
terminalService = addPlugin(tool, TerminalPlugin.class); terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService 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.injectDisplayOutput(TEST_CONTENTS);
term.provider.findDialog.txtFind.setText("term"); term.provider.findDialog.txtFind.setText("term");
@ -184,9 +215,9 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerGUITest {
terminalService = addPlugin(tool, TerminalPlugin.class); terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService 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.injectDisplayOutput(TEST_CONTENTS);
term.provider.findDialog.txtFind.setText("term"); term.provider.findDialog.txtFind.setText("term");
@ -216,9 +247,9 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerGUITest {
terminalService = addPlugin(tool, TerminalPlugin.class); terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService 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.injectDisplayOutput(TEST_CONTENTS);
term.provider.findDialog.txtFind.setText("term"); term.provider.findDialog.txtFind.setText("term");
@ -245,9 +276,9 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerGUITest {
terminalService = addPlugin(tool, TerminalPlugin.class); terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService 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.injectDisplayOutput(TEST_CONTENTS);
term.provider.findDialog.txtFind.setText("o?term"); term.provider.findDialog.txtFind.setText("o?term");
@ -283,9 +314,9 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerGUITest {
terminalService = addPlugin(tool, TerminalPlugin.class); terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService 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.injectDisplayOutput(TEST_CONTENTS);
term.provider.findDialog.txtFind.setText("term"); term.provider.findDialog.txtFind.setText("term");
@ -315,9 +346,9 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerGUITest {
terminalService = addPlugin(tool, TerminalPlugin.class); terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService 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.injectDisplayOutput(TEST_CONTENTS);
term.provider.findDialog.txtFind.setText("term"); term.provider.findDialog.txtFind.setText("term");
@ -347,9 +378,9 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerGUITest {
terminalService = addPlugin(tool, TerminalPlugin.class); terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService 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.injectDisplayOutput(TEST_CONTENTS);
term.provider.findDialog.txtFind.setText("o?term"); term.provider.findDialog.txtFind.setText("o?term");
@ -378,4 +409,190 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerGUITest {
dialog.close(); 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());
}
}
} }