GP-0: Fix frame selection in gdb-13.1. CI issues after upgrade.

This commit is contained in:
Dan 2023-08-10 15:04:42 -04:00
parent d8f163b542
commit dab3e1aa57
19 changed files with 362 additions and 78 deletions

View File

@ -113,6 +113,34 @@ public class GdbCommandDoneEvent extends AbstractGdbCompletedCommandEvent {
return tids;
}
/**
* Check if a new thread is specified
*
* @return the new thread id, or null if unspecified
*/
public Integer checkNewThreadId() {
String newTid = getInfo().getString("new-thread-id");
if (newTid == null) {
return null;
}
try {
return Integer.parseInt(newTid);
}
catch (NumberFormatException e) {
Msg.error(this, "Unexpected thread id in: " + newTid);
}
return null;
}
/**
* Check if a new frame is specified
*
* @return the new frame, or null if unspecified
*/
public GdbMiFieldList checkFrame() {
return getInfo().getFieldList("frame");
}
/**
* Assume a value is specified, and get it as a string
*

View File

@ -38,8 +38,10 @@ import agent.gdb.manager.evt.*;
import agent.gdb.manager.impl.cmd.*;
import agent.gdb.manager.impl.cmd.GdbConsoleExecCommand.CompletesWithRunning;
import agent.gdb.manager.parsing.GdbMiParser;
import agent.gdb.manager.parsing.GdbMiParser.GdbMiFieldList;
import agent.gdb.manager.parsing.GdbParsingUtils.GdbParseError;
import agent.gdb.pty.*;
import agent.gdb.pty.PtyChild.Echo;
import agent.gdb.pty.windows.AnsiBufferedInputStream;
import ghidra.GhidraApplicationLayout;
import ghidra.async.*;
@ -652,7 +654,9 @@ public class GdbManagerImpl implements GdbManager {
if (gdbCmd != null) {
iniThread = new PtyThread(ptyFactory.openpty(), Channel.STDOUT, null);
gdb = iniThread.pty.getChild().session(fullargs.toArray(new String[] {}), null);
Msg.info(this, "Starting gdb with: " + fullargs);
gdb =
iniThread.pty.getChild().session(fullargs.toArray(new String[] {}), null, Echo.OFF);
gdbWaiter = new Thread(this::waitGdbExit, "GDB WaitExit");
gdbWaiter.start();
@ -675,7 +679,7 @@ public class GdbManagerImpl implements GdbManager {
cliThread.writer.print("set pagination off" + newLine);
String ptyName;
try {
ptyName = Objects.requireNonNull(mi2Pty.getChild().nullSession());
ptyName = Objects.requireNonNull(mi2Pty.getChild().nullSession(Echo.OFF));
}
catch (UnsupportedOperationException e) {
throw new IOException(
@ -705,7 +709,7 @@ public class GdbManagerImpl implements GdbManager {
}
else {
Pty mi2Pty = ptyFactory.openpty();
String mi2PtyName = mi2Pty.getChild().nullSession();
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);
mi2Thread.setName("GDB Read MI2");
@ -1445,6 +1449,21 @@ public class GdbManagerImpl implements GdbManager {
}
}
private void emitNewThreadFrameIfSpecified(GdbCommandDoneEvent evt) {
Integer newTid = evt.checkNewThreadId();
if (newTid == null) {
return;
}
GdbThreadImpl thread = threads.get(newTid);
if (thread == null) {
return;
}
GdbMiFieldList newFrame = evt.checkFrame();
GdbStackFrameImpl frame =
newFrame == null ? null : GdbStackFrameImpl.fromFieldList(thread, newFrame);
event(() -> listenersEvent.fire.threadSelected(thread, frame, evt), "command-done");
}
/**
* Handler for "^done"
*
@ -1452,6 +1471,7 @@ public class GdbManagerImpl implements GdbManager {
* @param v nothing
*/
protected void processCommandDone(GdbCommandDoneEvent evt, Void v) {
emitNewThreadFrameIfSpecified(evt);
checkClaimed(evt);
}

View File

@ -106,8 +106,41 @@ public class GdbStackFrameImpl implements GdbStackFrame {
@Override
public CompletableFuture<Void> setActive(boolean internal) {
return manager
.execute(new GdbSetActiveThreadCommand(manager, thread.getId(), level, internal));
/**
* Since gdb-13.1, it seems there is no longer a way to select the thread and frame in a
* single command. I think it's a bug, but it's hard to tell what their intended behavior
* is. Take the following example MI command:
*
* <pre>
* -interpreter-exec --thread 1 console "frame 2"
* </pre>
*
* This will produce console output and an MI event indicating a frame context change:
*
* <pre>
* ~"#2 0x... in ... ()\n"
* =thread-selected,id="1",frame={level="2",...}
* </pre>
*
* However, the console has not actually changed context. If we then issue the {@code frame}
* command, we get:
*
* <pre>
* &"frame\n" ~"#0 0x... in read () from /.../libc.so.6\n"
* ^done
* (gdb)
* </pre>
*
* I'd expect either the context change to persist, or there not to be an event reported.
* Until or unless this is fixed, we will have to select the thread using
* {@code -thread-select}, then the frame using {@code -interpreter exec console "frame"}.
* Even if it is fixed, because we code to the least common denominator, we'll probably
* leave the two-command approach in place.
*/
return thread.setActive(true).thenCompose(__ -> {
return manager.execute(
new GdbSetActiveFrameCommand(manager, null, level, internal));
});
}
@Override

View File

@ -135,7 +135,7 @@ public class GdbThreadImpl implements GdbThread {
@Override
public CompletableFuture<Void> setActive(boolean internal) {
// Bypass the select-me-first logic
return manager.execute(new GdbSetActiveThreadCommand(manager, id, null, internal));
return manager.execute(new GdbSetActiveThreadCommand(manager, id, internal));
}
@Override

View File

@ -105,6 +105,8 @@ public abstract class AbstractGdbCommand<T> implements GdbCommand<T> {
* will likely type "start" into the existing CLI. Thus, we have to be careful not to let
* spurious {@code ^running} command-completion events actually complete any command, except
* ones where we expect that result. This seems a bug in GDB to me.
*
* UPDATE: It looks like this will be fixed in gdb-14.
*/
if (evt instanceof GdbCommandRunningEvent) {
return false;

View File

@ -0,0 +1,67 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.manager.impl.cmd;
import agent.gdb.manager.evt.GdbCommandDoneEvent;
import agent.gdb.manager.evt.GdbThreadSelectedEvent;
import agent.gdb.manager.impl.*;
public class GdbSetActiveFrameCommand extends AbstractGdbCommandWithThreadId<Void> {
private final Integer frameId;
private final boolean internal;
/**
* Select the given thread and frame
*
* @param manager the manager to execute the command
* @param threadId the desired thread Id
* @param frameId the desired frame level
* @param internal true to prevent announcement of the change
*/
public GdbSetActiveFrameCommand(GdbManagerImpl manager, Integer threadId, int frameId,
boolean internal) {
super(manager, threadId);
this.frameId = frameId;
this.internal = internal;
}
@Override
protected String encode(String threadPart) {
return "-interpreter-exec" + threadPart + " console \"frame " + frameId + "\"";
}
@Override
public boolean handle(GdbEvent<?> evt, GdbPendingCommand<?> pending) {
if (super.handle(evt, pending)) {
return true;
}
else if (evt instanceof GdbThreadSelectedEvent) {
pending.claim(evt);
}
return false;
}
@Override
public Void complete(GdbPendingCommand<?> pending) {
pending.checkCompletion(GdbCommandDoneEvent.class);
return null;
}
@Override
public boolean isFocusInternallyDriven() {
return internal;
}
}

View File

@ -15,38 +15,31 @@
*/
package agent.gdb.manager.impl.cmd;
import agent.gdb.manager.evt.*;
import agent.gdb.manager.evt.GdbCommandDoneEvent;
import agent.gdb.manager.evt.GdbThreadSelectedEvent;
import agent.gdb.manager.impl.*;
import agent.gdb.manager.parsing.GdbMiParser.GdbMiFieldList;
public class GdbSetActiveThreadCommand extends AbstractGdbCommandWithThreadAndFrameId<Void> {
public class GdbSetActiveThreadCommand extends AbstractGdbCommand<Void> {
private final int threadId;
private final boolean internal;
/**
* Select the given thread and frame level
*
* <p>
* To simply select a thread, you should use frame 0 as the default.
* Select the given thread
*
* @param manager the manager to execute the command
* @param threadId the desired thread Id
* @param frameId the desired frame level
* @param internal true to prevent announcement of the change
*/
public GdbSetActiveThreadCommand(GdbManagerImpl manager, int threadId, Integer frameId,
boolean internal) {
super(manager, threadId, frameId);
public GdbSetActiveThreadCommand(GdbManagerImpl manager, int threadId, boolean internal) {
super(manager);
this.threadId = threadId;
this.internal = internal;
}
@Override
public String encode(String threadPart, String framePart) {
/**
* Yes, it's a bit redundant to use {@code --thread} here, but this allows frame selection
* via {@code --frame} as well. Granted {@code -stack-select-frame} may be available, it
* doesn't appear to produce notifications, and so I've opted not to use it.
*/
return "-thread-select" + threadPart + framePart + " " + threadId;
public String encode() {
return "-thread-select " + threadId;
}
@Override
@ -62,19 +55,7 @@ public class GdbSetActiveThreadCommand extends AbstractGdbCommandWithThreadAndFr
@Override
public Void complete(GdbPendingCommand<?> pending) {
GdbCommandDoneEvent done = pending.checkCompletion(GdbCommandDoneEvent.class);
GdbThreadSelectedEvent already = pending.getFirstOf(GdbThreadSelectedEvent.class);
if (already != null) {
return null;
}
// Otherwise, we just changed frames within a thread. Fire the event ourselves.
GdbThreadImpl thread = manager.getThread(threadId);
GdbMiFieldList fields = done.getInfo().getFieldList("frame");
if (fields == null) { // Uhhh... I guess we'll have to do without
return null;
}
GdbStackFrameImpl frame = GdbStackFrameImpl.fromFieldList(thread, fields);
manager.doThreadSelected(thread, frame, done.getCause());
pending.checkCompletion(GdbCommandDoneEvent.class);
return null;
}

View File

@ -189,7 +189,10 @@ public class GdbModelTargetSession extends DefaultTargetModelRoot
GdbModelTargetInferior inf = inferiors.getTargetInferior(thread.getInferior());
GdbModelTargetThread t = inf.threads.getTargetThread(thread);
if (frame == null) {
setFocus(t);
GdbModelSelectableObject curFocus = getFocus();
if (curFocus != null && !PathUtils.isAncestor(t.getPath(), curFocus.getPath())) {
setFocus(t);
}
return;
}
GdbModelTargetStackFrame f = t.stack.getTargetFrame(frame);

View File

@ -16,41 +16,73 @@
package agent.gdb.pty;
import java.io.IOException;
import java.util.Map;
import java.util.*;
/**
* The child (UNIX "slave") end of a pseudo-terminal
*/
public interface PtyChild extends PtyEndpoint {
/**
* A terminal mode flag
*/
interface TermMode {
}
/**
* Mode flag for local echo
*/
enum Echo implements TermMode {
/**
* Input is echoed to output by the terminal itself.
*/
ON,
/**
* No local echo.
*/
OFF;
}
/**
* Spawn a subprocess in a new session whose controlling tty is this pseudo-terminal
*
* <p>
* This method or {@link #nullSession()} can only be invoked once per pty.
* This method or {@link #nullSession(Collection)} can only be invoked once per pty.
*
* @param args the image path and arguments
* @param env the environment
* @param mode the terminal mode. If a mode is not implemented, it may be silently ignored.
* @return a handle to the subprocess
* @throws IOException if the session could not be started
*/
PtySession session(String[] args, Map<String, String> env) throws IOException;
PtySession session(String[] args, Map<String, String> env, Collection<TermMode> mode)
throws IOException;
default PtySession session(String[] args, Map<String, String> env, TermMode... mode)
throws IOException {
return session(args, env, List.of(mode));
}
/**
* Start a session without a real leader, instead obtaining the pty's name
*
* <p>
* This method or {@link #session(String[], Map)} can only be invoked once per pty. It must be
* called before anyone reads the parent's output stream, since obtaining the filename may be
* implemented by the parent sending commands to its child.
* This method or {@link #session(String[], Map, Collection)} can only be invoked once per pty.
* It must be called before anyone reads the parent's output stream, since obtaining the
* filename may be implemented by the parent sending commands to its child.
*
* <p>
* If the child end of the pty is on a remote system, this should be the file (or other
* resource) name as it would be accessed on that remote system.
*
* @param mode the terminal mode. If a mode is not implemented, it may be silently ignored.
* @return the file name
* @throws IOException if the session could not be started or the pty name could not be
* determined
*/
String nullSession() throws IOException;
String nullSession(Collection<TermMode> mode) throws IOException;
default String nullSession(TermMode... mode) throws IOException {
return nullSession(List.of(mode));
}
}

View File

@ -65,6 +65,9 @@ public class FdInputStream extends InputStream {
if (closed) {
throw new IOException("Stream closed");
}
if (len == 0) {
return 0;
}
Memory buf = new Memory(len);
int ret = LIB_POSIX.read(fd, buf, len);
buf.read(0, b, off, ret);

View File

@ -23,10 +23,13 @@ import java.util.*;
import agent.gdb.pty.PtyChild;
import agent.gdb.pty.PtySession;
import agent.gdb.pty.linux.PosixC.Termios;
import agent.gdb.pty.local.LocalProcessPtySession;
import ghidra.util.Msg;
public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild {
static final PosixC LIB_POSIX = PosixC.INSTANCE;
private final String name;
LinuxPtyChild(int fd, String name) {
@ -35,7 +38,8 @@ public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild {
}
@Override
public String nullSession() {
public String nullSession(Collection<TermMode> mode) {
applyMode(mode);
return name;
}
@ -53,19 +57,15 @@ public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild {
* received by the leader by mistake if sent immediately upon spawning a new session.
* Users should send a simple command, e.g., "echo", to confirm that the requested
* program is active before sending special characters.
*
* @param args the image path and arguments
* @param env the environment
* @return a handle to the subprocess
* @throws IOException
*/
@Override
public PtySession session(String[] args, Map<String, String> env) throws IOException {
return sessionUsingJavaLeader(args, env);
public PtySession session(String[] args, Map<String, String> env, Collection<TermMode> mode)
throws IOException {
return sessionUsingJavaLeader(args, env, mode);
}
protected PtySession sessionUsingJavaLeader(String[] args, Map<String, String> env)
throws IOException {
protected PtySession sessionUsingJavaLeader(String[] args, Map<String, String> env,
Collection<TermMode> mode) throws IOException {
final List<String> argsList = new ArrayList<>();
String javaCommand =
System.getProperty("java.home") + File.separator + "bin" + File.separator + "java";
@ -82,6 +82,8 @@ public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild {
}
builder.inheritIO();
applyMode(mode);
try {
return new LocalProcessPtySession(builder.start());
}
@ -148,4 +150,17 @@ public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild {
return null;
}
private void applyMode(Collection<TermMode> mode) {
if (mode.contains(Echo.OFF)) {
disableEcho();
}
}
private void disableEcho() {
Termios.ByReference tmios = new Termios.ByReference();
LIB_POSIX.tcgetattr(fd, tmios);
tmios.c_lflag &= ~Termios.ECHO;
LIB_POSIX.tcsetattr(fd, Termios.TCSANOW, tmios);
}
}

View File

@ -21,12 +21,12 @@ import java.io.OutputStream;
import agent.gdb.pty.PtyEndpoint;
public class LinuxPtyEndpoint implements PtyEndpoint {
//private final int fd;
protected final int fd;
private final FdOutputStream outputStream;
private final FdInputStream inputStream;
LinuxPtyEndpoint(int fd) {
//this.fd = fd;
this.fd = fd;
this.outputStream = new FdOutputStream(fd);
this.inputStream = new FdInputStream(fd);
}

View File

@ -16,6 +16,7 @@
package agent.gdb.pty.linux;
import com.sun.jna.*;
import com.sun.jna.Structure.FieldOrder;
/**
* Interface for POSIX functions in libc
@ -24,6 +25,29 @@ import com.sun.jna.*;
* The functions are not documented here. Instead see the POSIX manual pages.
*/
public interface PosixC extends Library {
@FieldOrder({ "c_iflag", "c_oflag", "c_cflag", "c_lflag", "c_line", "c_cc", "c_ispeed",
"c_ospeed" })
class Termios extends Structure {
public static final int TCSANOW = 0;
public static final int ECHO = 0000010; // Octal
public int c_iflag;
public int c_oflag;
public int c_cflag;
public int c_lflag;
public byte c_line;
public byte[] c_cc = new byte[32];
public int c_ispeed;
public int c_ospeed;
public static class ByReference extends Termios implements Structure.ByReference {
}
}
/**
* The bare library without error handling
*
@ -71,6 +95,16 @@ public interface PosixC extends Library {
public int execv(String path, String[] argv) {
return Err.checkLt0(BARE.execv(path, argv));
}
@Override
public int tcgetattr(int fd, Termios.ByReference termios_p) {
return Err.checkLt0(BARE.tcgetattr(fd, termios_p));
}
@Override
public int tcsetattr(int fd, int optional_actions, Termios.ByReference termios_p) {
return Err.checkLt0(BARE.tcsetattr(fd, optional_actions, termios_p));
}
};
String strerror(int errnum);
@ -88,4 +122,8 @@ public interface PosixC extends Library {
int dup2(int oldfd, int newfd);
int execv(String path, String[] argv);
int tcgetattr(int fd, Termios.ByReference termios_p);
int tcsetattr(int fd, int optional_actions, Termios.ByReference termios_p);
}

View File

@ -16,13 +16,11 @@
package agent.gdb.pty.ssh;
import java.io.*;
import java.util.Arrays;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;
import javax.help.UnsupportedOperationException;
import com.jcraft.jsch.*;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSchException;
import agent.gdb.pty.PtyChild;
import ghidra.dbg.util.ShellUtils;
@ -38,8 +36,23 @@ public class SshPtyChild extends SshPtyEndpoint implements PtyChild {
this.channel = channel;
}
private String sttyString(Collection<TermMode> mode) {
StringBuilder sb = new StringBuilder();
if (mode.contains(Echo.OFF)) {
sb.append("-echo ");
}
else if (mode.contains(Echo.ON)) {
sb.append("echo ");
}
if (sb.isEmpty()) {
return "";
}
return "stty " + sb + "&& ";
}
@Override
public SshPtySession session(String[] args, Map<String, String> env) throws IOException {
public SshPtySession session(String[] args, Map<String, String> env, Collection<TermMode> mode)
throws IOException {
/**
* TODO: This syntax assumes a UNIX-style shell, and even among them, this may not be
* universal. This certainly works for my version of bash :)
@ -52,7 +65,7 @@ public class SshPtyChild extends SshPtyEndpoint implements PtyChild {
.collect(Collectors.joining(" ")) +
" ";
String cmdStr = ShellUtils.generateLine(Arrays.asList(args));
channel.setCommand(envStr + cmdStr);
channel.setCommand(sttyString(mode) + envStr + cmdStr);
try {
channel.connect();
}
@ -62,11 +75,11 @@ public class SshPtyChild extends SshPtyEndpoint implements PtyChild {
return new SshPtySession(channel);
}
private String getTtyNameAndStartNullSession() throws IOException {
private String getTtyNameAndStartNullSession(Collection<TermMode> mode) throws IOException {
// NB. UNIX sleep is only required to support integer durations
channel.setCommand(
("sh -c 'tty && ctrlc() { echo; } && trap ctrlc INT && while true; do sleep " +
Integer.MAX_VALUE + "; done'"));
channel.setCommand(sttyString(mode) +
"sh -c 'tty && ctrlc() { echo; } && trap ctrlc INT && while true; do sleep " +
Integer.MAX_VALUE + "; done'");
try {
channel.connect();
}
@ -85,9 +98,9 @@ public class SshPtyChild extends SshPtyEndpoint implements PtyChild {
}
@Override
public String nullSession() throws IOException {
public String nullSession(Collection<TermMode> mode) throws IOException {
if (name == null) {
this.name = getTtyNameAndStartNullSession();
this.name = getTtyNameAndStartNullSession(mode);
if ("".equals(name)) {
throw new IOException("Could not determine child remote tty name");
}

View File

@ -16,8 +16,7 @@
package agent.gdb.pty.windows;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
import java.util.*;
import com.sun.jna.*;
import com.sun.jna.platform.win32.Kernel32;
@ -76,10 +75,12 @@ public class ConPtyChild extends ConPtyEndpoint implements PtyChild {
}
@Override
public LocalWindowsNativeProcessPtySession session(String[] args, Map<String, String> env)
throws IOException {
public LocalWindowsNativeProcessPtySession session(String[] args, Map<String, String> env,
Collection<TermMode> mode) throws IOException {
/**
* TODO: How to incorporate environment into CreateProcess?
*
* TODO: How to control local echo?
*/
STARTUPINFOEX si = prepareStartupInfo();
@ -105,7 +106,7 @@ public class ConPtyChild extends ConPtyEndpoint implements PtyChild {
}
@Override
public String nullSession() throws IOException {
public String nullSession(Collection<TermMode> mode) throws IOException {
throw new UnsupportedOperationException("ConPTY does not have a name");
}
}

View File

@ -17,7 +17,7 @@ package agent.gdb.model.gadp;
import agent.gdb.model.AbstractModelForGdbSessionAttacherTest;
public class GadpModelForGdbSessiopnAttacherTest extends AbstractModelForGdbSessionAttacherTest {
public class GadpModelForGdbSessionAttacherTest extends AbstractModelForGdbSessionAttacherTest {
@Override
public ModelHost modelHost() throws Throwable {
return new GadpGdbModelHost();

View File

@ -17,7 +17,7 @@ package agent.gdb.model.invm;
import agent.gdb.model.AbstractModelForGdbSessionAttacherTest;
public class InVmModelForGdbSessiopnAttacherTest extends AbstractModelForGdbSessionAttacherTest {
public class InVmModelForGdbSessionAttacherTest extends AbstractModelForGdbSessionAttacherTest {
@Override
public ModelHost modelHost() throws Throwable {
return new InVmGdbModelHost();

View File

@ -15,8 +15,7 @@
*/
package agent.gdb.pty.linux;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.*;
import static org.junit.Assume.assumeTrue;
import java.io.*;
@ -26,6 +25,7 @@ import org.junit.Before;
import org.junit.Test;
import agent.gdb.pty.AbstractPtyTest;
import agent.gdb.pty.PtyChild.Echo;
import agent.gdb.pty.PtySession;
import ghidra.dbg.testutil.DummyProc;
import ghidra.framework.OperatingSystem;
@ -177,4 +177,38 @@ public class LinuxPtyTest extends AbstractPtyTest {
assertEquals(3, bash.waitExited());
}
}
@Test
public void testLocalEchoOn() throws IOException {
try (LinuxPty pty = LinuxPty.openpty()) {
pty.getChild().nullSession();
PrintWriter writer = new PrintWriter(pty.getParent().getOutputStream());
BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
writer.println("Hello, World!");
writer.flush();
assertEquals("Hello, World!", reader.readLine());
}
}
@Test
public void testLocalEchoOff() throws IOException {
try (LinuxPty pty = LinuxPty.openpty()) {
pty.getChild().nullSession(Echo.OFF);
PrintWriter writerP = new PrintWriter(pty.getParent().getOutputStream());
PrintWriter writerC = new PrintWriter(pty.getChild().getOutputStream());
BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
writerP.println("Hello, World!");
writerP.flush();
writerC.println("Good bye!");
writerC.flush();
assertEquals("Good bye!", reader.readLine());
}
}
}

View File

@ -23,6 +23,7 @@ import java.io.*;
import org.junit.Before;
import org.junit.Test;
import agent.gdb.pty.PtyChild.Echo;
import agent.gdb.pty.PtySession;
import ghidra.app.script.AskDialog;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
@ -85,4 +86,17 @@ public class SshPtyTest extends AbstractGhidraHeadedIntegrationTest {
assertEquals(0, bash.waitExited());
}
}
@Test
public void testDisableEcho() throws IOException, InterruptedException {
try (SshPty pty = factory.openpty()) {
PtySession bash =
pty.getChild().session(new String[] { "bash" }, null, Echo.OFF);
OutputStream out = pty.getParent().getOutputStream();
out.write("exit\n".getBytes("UTF-8"));
out.flush();
new StreamPumper(pty.getParent().getInputStream(), System.out).start();
assertEquals(0, bash.waitExited());
}
}
}