Skip to content

Commit 5c4cb32

Browse files
committed
[lldb/qemu] Add support for pty redirection
Lldb uses a pty to read/write to the standard input and output of the debugged process. For host processes this would be automatically set up by Target::FinalizeFileActions. The Qemu platform is in a unique position of not really being a host platform, but not being remote either. It reports IsHost() = false, but it is sufficiently host-like that we can use the usual pty mechanism. This patch adds the necessary glue code to enable pty redirection. It includes a small refactor of Target::FinalizeFileActions and ProcessLaunchInfo::SetUpPtyRedirection to reduce the amount of boilerplate that would need to be copied. I will note that qemu is not able to separate output from the emulated program from the output of the emulator itself, so the two will arrive intertwined. Normally this should not be a problem since qemu should not produce any output during regular operation, but some output can slip through in case of errors. This situation should be pretty obvious (to a human), and it is the best we can do anyway. For testing purposes, and inspired by lldb-server tests, I have extended the mock emulator with the ability "program" the behavior of the "emulated" program via command-line arguments. Differential Revision: https://reviews.llvm.org/D114796
1 parent 85578db commit 5c4cb32

File tree

5 files changed

+146
-29
lines changed

5 files changed

+146
-29
lines changed

lldb/source/Host/common/ProcessLaunchInfo.cpp

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,14 @@ void ProcessLaunchInfo::SetDetachOnError(bool enable) {
212212

213213
llvm::Error ProcessLaunchInfo::SetUpPtyRedirection() {
214214
Log *log = GetLogIfAllCategoriesSet(LIBLLDB_LOG_PROCESS);
215+
216+
bool stdin_free = GetFileActionForFD(STDIN_FILENO) == nullptr;
217+
bool stdout_free = GetFileActionForFD(STDOUT_FILENO) == nullptr;
218+
bool stderr_free = GetFileActionForFD(STDERR_FILENO) == nullptr;
219+
bool any_free = stdin_free || stdout_free || stderr_free;
220+
if (!any_free)
221+
return llvm::Error::success();
222+
215223
LLDB_LOG(log, "Generating a pty to use for stdin/out/err");
216224

217225
int open_flags = O_RDWR | O_NOCTTY;
@@ -226,19 +234,13 @@ llvm::Error ProcessLaunchInfo::SetUpPtyRedirection() {
226234

227235
const FileSpec secondary_file_spec(m_pty->GetSecondaryName());
228236

229-
// Only use the secondary tty if we don't have anything specified for
230-
// input and don't have an action for stdin
231-
if (GetFileActionForFD(STDIN_FILENO) == nullptr)
237+
if (stdin_free)
232238
AppendOpenFileAction(STDIN_FILENO, secondary_file_spec, true, false);
233239

234-
// Only use the secondary tty if we don't have anything specified for
235-
// output and don't have an action for stdout
236-
if (GetFileActionForFD(STDOUT_FILENO) == nullptr)
240+
if (stdout_free)
237241
AppendOpenFileAction(STDOUT_FILENO, secondary_file_spec, false, true);
238242

239-
// Only use the secondary tty if we don't have anything specified for
240-
// error and don't have an action for stderr
241-
if (GetFileActionForFD(STDERR_FILENO) == nullptr)
243+
if (stderr_free)
242244
AppendOpenFileAction(STDERR_FILENO, secondary_file_spec, false, true);
243245
return llvm::Error::success();
244246
}

lldb/source/Plugins/Platform/QemuUser/PlatformQemuUser.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,11 @@ lldb::ProcessSP PlatformQemuUser::DebugProcess(ProcessLaunchInfo &launch_info,
126126
launch_info.SetMonitorProcessCallback(ProcessLaunchInfo::NoOpMonitorCallback,
127127
false);
128128

129+
// This is automatically done for host platform in
130+
// Target::FinalizeFileActions, but we're not a host platform.
131+
llvm::Error Err = launch_info.SetUpPtyRedirection();
132+
LLDB_LOG_ERROR(log, std::move(Err), "SetUpPtyRedirection failed: {0}");
133+
129134
error = Host::LaunchProcess(launch_info);
130135
if (error.Fail())
131136
return nullptr;
@@ -134,6 +139,7 @@ lldb::ProcessSP PlatformQemuUser::DebugProcess(ProcessLaunchInfo &launch_info,
134139
launch_info.GetListener(),
135140
process_gdb_remote::ProcessGDBRemote::GetPluginNameStatic(), nullptr,
136141
true);
142+
137143
ListenerSP listener_sp =
138144
Listener::MakeListener("lldb.platform_qemu_user.debugprocess");
139145
launch_info.SetHijackListener(listener_sp);
@@ -143,6 +149,11 @@ lldb::ProcessSP PlatformQemuUser::DebugProcess(ProcessLaunchInfo &launch_info,
143149
if (error.Fail())
144150
return nullptr;
145151

152+
if (launch_info.GetPTY().GetPrimaryFileDescriptor() !=
153+
PseudoTerminal::invalid_fd)
154+
process_sp->SetSTDIOFileDescriptor(
155+
launch_info.GetPTY().ReleasePrimaryFileDescriptor());
156+
146157
process_sp->WaitForProcessToStop(llvm::None, nullptr, false, listener_sp);
147158
return process_sp;
148159
}

lldb/source/Target/Target.cpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3321,8 +3321,7 @@ void Target::FinalizeFileActions(ProcessLaunchInfo &info) {
33213321
err_file_spec);
33223322
}
33233323

3324-
if (default_to_use_pty &&
3325-
(!in_file_spec || !out_file_spec || !err_file_spec)) {
3324+
if (default_to_use_pty) {
33263325
llvm::Error Err = info.SetUpPtyRedirection();
33273326
LLDB_LOG_ERROR(log, std::move(Err), "SetUpPtyRedirection failed: {0}");
33283327
}

lldb/test/API/qemu/TestQemuLaunch.py

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import stat
77
import sys
88
from textwrap import dedent
9+
import lldbsuite.test.lldbutil
910
from lldbsuite.test.lldbtest import *
1011
from lldbsuite.test.decorators import *
1112
from lldbsuite.test.gdbclientutils import *
@@ -46,7 +47,7 @@ def test_basic_launch(self):
4647
self.build()
4748
exe = self.getBuildArtifact()
4849

49-
# Create a target using out platform
50+
# Create a target using our platform
5051
error = lldb.SBError()
5152
target = self.dbg.CreateTarget(exe, '', 'qemu-user', False, error)
5253
self.assertSuccess(error)
@@ -55,7 +56,7 @@ def test_basic_launch(self):
5556
# "Launch" the process. Our fake qemu implementation will pretend it
5657
# immediately exited.
5758
process = target.LaunchSimple(
58-
[self.getBuildArtifact("state.log"), "arg2", "arg3"], None, None)
59+
["dump:" + self.getBuildArtifact("state.log")], None, None)
5960
self.assertIsNotNone(process)
6061
self.assertEqual(process.GetState(), lldb.eStateExited)
6162
self.assertEqual(process.GetExitStatus(), 0x47)
@@ -64,7 +65,84 @@ def test_basic_launch(self):
6465
with open(self.getBuildArtifact("state.log")) as s:
6566
state = json.load(s)
6667
self.assertEqual(state["program"], self.getBuildArtifact())
67-
self.assertEqual(state["rest"], ["arg2", "arg3"])
68+
self.assertEqual(state["args"],
69+
["dump:" + self.getBuildArtifact("state.log")])
70+
71+
def test_stdio_pty(self):
72+
self.build()
73+
exe = self.getBuildArtifact()
74+
75+
# Create a target using our platform
76+
error = lldb.SBError()
77+
target = self.dbg.CreateTarget(exe, '', 'qemu-user', False, error)
78+
self.assertSuccess(error)
79+
80+
info = lldb.SBLaunchInfo([
81+
"stdin:stdin",
82+
"stdout:STDOUT CONTENT\n",
83+
"stderr:STDERR CONTENT\n",
84+
"dump:" + self.getBuildArtifact("state.log"),
85+
])
86+
87+
listener = lldb.SBListener("test_stdio")
88+
info.SetListener(listener)
89+
90+
self.dbg.SetAsync(True)
91+
process = target.Launch(info, error)
92+
self.assertSuccess(error)
93+
lldbutil.expect_state_changes(self, listener, process,
94+
[lldb.eStateRunning])
95+
96+
process.PutSTDIN("STDIN CONTENT\n")
97+
98+
lldbutil.expect_state_changes(self, listener, process,
99+
[lldb.eStateExited])
100+
101+
# Echoed stdin, stdout and stderr. With a pty we cannot split standard
102+
# output and error.
103+
self.assertEqual(process.GetSTDOUT(1000),
104+
"STDIN CONTENT\r\nSTDOUT CONTENT\r\nSTDERR CONTENT\r\n")
105+
with open(self.getBuildArtifact("state.log")) as s:
106+
state = json.load(s)
107+
self.assertEqual(state["stdin"], "STDIN CONTENT\n")
108+
109+
def test_stdio_redirect(self):
110+
self.build()
111+
exe = self.getBuildArtifact()
112+
113+
# Create a target using our platform
114+
error = lldb.SBError()
115+
target = self.dbg.CreateTarget(exe, '', 'qemu-user', False, error)
116+
self.assertSuccess(error)
117+
118+
info = lldb.SBLaunchInfo([
119+
"stdin:stdin",
120+
"stdout:STDOUT CONTENT",
121+
"stderr:STDERR CONTENT",
122+
"dump:" + self.getBuildArtifact("state.log"),
123+
])
124+
125+
info.AddOpenFileAction(0, self.getBuildArtifact("stdin.txt"),
126+
True, False)
127+
info.AddOpenFileAction(1, self.getBuildArtifact("stdout.txt"),
128+
False, True)
129+
info.AddOpenFileAction(2, self.getBuildArtifact("stderr.txt"),
130+
False, True)
131+
132+
with open(self.getBuildArtifact("stdin.txt"), "w") as f:
133+
f.write("STDIN CONTENT")
134+
135+
process = target.Launch(info, error)
136+
self.assertSuccess(error)
137+
self.assertEqual(process.GetState(), lldb.eStateExited)
138+
139+
with open(self.getBuildArtifact("stdout.txt")) as f:
140+
self.assertEqual(f.read(), "STDOUT CONTENT")
141+
with open(self.getBuildArtifact("stderr.txt")) as f:
142+
self.assertEqual(f.read(), "STDERR CONTENT")
143+
with open(self.getBuildArtifact("state.log")) as s:
144+
state = json.load(s)
145+
self.assertEqual(state["stdin"], "STDIN CONTENT")
68146

69147
def test_bad_emulator_path(self):
70148
self.set_emulator_setting("emulator-path",

lldb/test/API/qemu/qemu.py

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,63 @@
1-
from textwrap import dedent
21
import argparse
32
import socket
43
import json
4+
import sys
55

66
import use_lldb_suite
77
from lldbsuite.test.gdbclientutils import *
88

9+
_description = """\
10+
Implements a fake qemu for testing purposes. The executable program
11+
is not actually run. Instead a very basic mock process is presented
12+
to lldb. This allows us to check the invocation parameters.
13+
14+
The behavior of the emulated "process" is controlled via its command line
15+
arguments, which should take the form of key:value pairs. Currently supported
16+
actions are:
17+
- dump: Dump the state of the emulator as a json dictionary. <value> specifies
18+
the target filename.
19+
- stdout: Write <value> to program stdout.
20+
- stderr: Write <value> to program stderr.
21+
- stdin: Read a line from stdin and store it in the emulator state. <value>
22+
specifies the dictionary key.
23+
"""
24+
925
class MyResponder(MockGDBServerResponder):
26+
def __init__(self, state):
27+
super().__init__()
28+
self._state = state
29+
1030
def cont(self):
31+
for a in self._state["args"]:
32+
action, data = a.split(":", 1)
33+
if action == "dump":
34+
with open(data, "w") as f:
35+
json.dump(self._state, f)
36+
elif action == "stdout":
37+
sys.stdout.write(data)
38+
elif action == "stderr":
39+
sys.stderr.write(data)
40+
elif action == "stdin":
41+
self._state[data] = sys.stdin.readline()
42+
else:
43+
print("Unknown action: %r\n" % a)
44+
return "X01"
1145
return "W47"
1246

1347
class FakeEmulator(MockGDBServer):
14-
def __init__(self, addr):
48+
def __init__(self, addr, state):
1549
super().__init__(UnixServerSocket(addr))
16-
self.responder = MyResponder()
50+
self.responder = MyResponder(state)
1751

1852
def main():
19-
parser = argparse.ArgumentParser(description=dedent("""\
20-
Implements a fake qemu for testing purposes. The executable program
21-
is not actually run. Instead a very basic mock process is presented
22-
to lldb. The emulated program must accept at least one argument.
23-
This should be a path where the emulator will dump its state. This
24-
allows us to check the invocation parameters.
25-
"""))
53+
parser = argparse.ArgumentParser(description=_description,
54+
formatter_class=argparse.RawDescriptionHelpFormatter)
2655
parser.add_argument('-g', metavar="unix-socket", required=True)
2756
parser.add_argument('program', help="The program to 'emulate'.")
28-
parser.add_argument('state_file', help="Where to dump the emulator state.")
29-
parsed, rest = parser.parse_known_args()
30-
with open(parsed.state_file, "w") as f:
31-
json.dump({"program":parsed.program, "rest":rest}, f)
57+
parser.add_argument("args", nargs=argparse.REMAINDER)
58+
args = parser.parse_args()
3259

33-
emulator = FakeEmulator(parsed.g)
60+
emulator = FakeEmulator(args.g, vars(args))
3461
emulator.run()
3562

3663
if __name__ == "__main__":

0 commit comments

Comments
 (0)