mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-06 03:50:02 +02:00
GP-3800: Get TerminalService working on Windows, too.
This commit is contained in:
parent
5bed356fd2
commit
a548e54075
37 changed files with 834 additions and 217 deletions
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ public class Handle implements AutoCloseable {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
public void close() {
|
||||
cleanable.clean();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue