diff --git a/lldb/source/Host/common/ProcessLaunchInfo.cpp b/lldb/source/Host/common/ProcessLaunchInfo.cpp --- a/lldb/source/Host/common/ProcessLaunchInfo.cpp +++ b/lldb/source/Host/common/ProcessLaunchInfo.cpp @@ -212,6 +212,14 @@ llvm::Error ProcessLaunchInfo::SetUpPtyRedirection() { Log *log = GetLogIfAllCategoriesSet(LIBLLDB_LOG_PROCESS); + + bool stdin_free = GetFileActionForFD(STDIN_FILENO) == nullptr; + bool stdout_free = GetFileActionForFD(STDOUT_FILENO) == nullptr; + bool stderr_free = GetFileActionForFD(STDERR_FILENO) == nullptr; + bool any_free = stdin_free || stdout_free || stderr_free; + if (!any_free) + return llvm::Error::success(); + LLDB_LOG(log, "Generating a pty to use for stdin/out/err"); int open_flags = O_RDWR | O_NOCTTY; @@ -226,19 +234,13 @@ const FileSpec secondary_file_spec(m_pty->GetSecondaryName()); - // Only use the secondary tty if we don't have anything specified for - // input and don't have an action for stdin - if (GetFileActionForFD(STDIN_FILENO) == nullptr) + if (stdin_free) AppendOpenFileAction(STDIN_FILENO, secondary_file_spec, true, false); - // Only use the secondary tty if we don't have anything specified for - // output and don't have an action for stdout - if (GetFileActionForFD(STDOUT_FILENO) == nullptr) + if (stdout_free) AppendOpenFileAction(STDOUT_FILENO, secondary_file_spec, false, true); - // Only use the secondary tty if we don't have anything specified for - // error and don't have an action for stderr - if (GetFileActionForFD(STDERR_FILENO) == nullptr) + if (stderr_free) AppendOpenFileAction(STDERR_FILENO, secondary_file_spec, false, true); return llvm::Error::success(); } diff --git a/lldb/source/Plugins/Platform/QemuUser/PlatformQemuUser.cpp b/lldb/source/Plugins/Platform/QemuUser/PlatformQemuUser.cpp --- a/lldb/source/Plugins/Platform/QemuUser/PlatformQemuUser.cpp +++ b/lldb/source/Plugins/Platform/QemuUser/PlatformQemuUser.cpp @@ -126,6 +126,11 @@ launch_info.SetMonitorProcessCallback(ProcessLaunchInfo::NoOpMonitorCallback, false); + // This is automatically done for host platform in + // Target::FinalizeFileActions, but we're not a host platform. + llvm::Error Err = launch_info.SetUpPtyRedirection(); + LLDB_LOG_ERROR(log, std::move(Err), "SetUpPtyRedirection failed: {0}"); + error = Host::LaunchProcess(launch_info); if (error.Fail()) return nullptr; @@ -134,6 +139,7 @@ launch_info.GetListener(), process_gdb_remote::ProcessGDBRemote::GetPluginNameStatic(), nullptr, true); + ListenerSP listener_sp = Listener::MakeListener("lldb.platform_qemu_user.debugprocess"); launch_info.SetHijackListener(listener_sp); @@ -143,6 +149,11 @@ if (error.Fail()) return nullptr; + if (launch_info.GetPTY().GetPrimaryFileDescriptor() != + PseudoTerminal::invalid_fd) + process_sp->SetSTDIOFileDescriptor( + launch_info.GetPTY().ReleasePrimaryFileDescriptor()); + process_sp->WaitForProcessToStop(llvm::None, nullptr, false, listener_sp); return process_sp; } diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp --- a/lldb/source/Target/Target.cpp +++ b/lldb/source/Target/Target.cpp @@ -3321,8 +3321,7 @@ err_file_spec); } - if (default_to_use_pty && - (!in_file_spec || !out_file_spec || !err_file_spec)) { + if (default_to_use_pty) { llvm::Error Err = info.SetUpPtyRedirection(); LLDB_LOG_ERROR(log, std::move(Err), "SetUpPtyRedirection failed: {0}"); } diff --git a/lldb/test/API/qemu/TestQemuLaunch.py b/lldb/test/API/qemu/TestQemuLaunch.py --- a/lldb/test/API/qemu/TestQemuLaunch.py +++ b/lldb/test/API/qemu/TestQemuLaunch.py @@ -6,6 +6,7 @@ import stat import sys from textwrap import dedent +import lldbsuite.test.lldbutil from lldbsuite.test.lldbtest import * from lldbsuite.test.decorators import * from lldbsuite.test.gdbclientutils import * @@ -46,7 +47,7 @@ self.build() exe = self.getBuildArtifact() - # Create a target using out platform + # Create a target using our platform error = lldb.SBError() target = self.dbg.CreateTarget(exe, '', 'qemu-user', False, error) self.assertSuccess(error) @@ -55,7 +56,7 @@ # "Launch" the process. Our fake qemu implementation will pretend it # immediately exited. process = target.LaunchSimple( - [self.getBuildArtifact("state.log"), "arg2", "arg3"], None, None) + ["dump:" + self.getBuildArtifact("state.log")], None, None) self.assertIsNotNone(process) self.assertEqual(process.GetState(), lldb.eStateExited) self.assertEqual(process.GetExitStatus(), 0x47) @@ -64,7 +65,84 @@ with open(self.getBuildArtifact("state.log")) as s: state = json.load(s) self.assertEqual(state["program"], self.getBuildArtifact()) - self.assertEqual(state["rest"], ["arg2", "arg3"]) + self.assertEqual(state["args"], + ["dump:" + self.getBuildArtifact("state.log")]) + + def test_stdio_pty(self): + self.build() + exe = self.getBuildArtifact() + + # Create a target using our platform + error = lldb.SBError() + target = self.dbg.CreateTarget(exe, '', 'qemu-user', False, error) + self.assertSuccess(error) + + info = lldb.SBLaunchInfo([ + "stdin:stdin", + "stdout:STDOUT CONTENT\n", + "stderr:STDERR CONTENT\n", + "dump:" + self.getBuildArtifact("state.log"), + ]) + + listener = lldb.SBListener("test_stdio") + info.SetListener(listener) + + self.dbg.SetAsync(True) + process = target.Launch(info, error) + self.assertSuccess(error) + lldbutil.expect_state_changes(self, listener, process, + [lldb.eStateRunning]) + + process.PutSTDIN("STDIN CONTENT\n") + + lldbutil.expect_state_changes(self, listener, process, + [lldb.eStateExited]) + + # Echoed stdin, stdout and stderr. With a pty we cannot split standard + # output and error. + self.assertEqual(process.GetSTDOUT(1000), + "STDIN CONTENT\r\nSTDOUT CONTENT\r\nSTDERR CONTENT\r\n") + with open(self.getBuildArtifact("state.log")) as s: + state = json.load(s) + self.assertEqual(state["stdin"], "STDIN CONTENT\n") + + def test_stdio_redirect(self): + self.build() + exe = self.getBuildArtifact() + + # Create a target using our platform + error = lldb.SBError() + target = self.dbg.CreateTarget(exe, '', 'qemu-user', False, error) + self.assertSuccess(error) + + info = lldb.SBLaunchInfo([ + "stdin:stdin", + "stdout:STDOUT CONTENT", + "stderr:STDERR CONTENT", + "dump:" + self.getBuildArtifact("state.log"), + ]) + + info.AddOpenFileAction(0, self.getBuildArtifact("stdin.txt"), + True, False) + info.AddOpenFileAction(1, self.getBuildArtifact("stdout.txt"), + False, True) + info.AddOpenFileAction(2, self.getBuildArtifact("stderr.txt"), + False, True) + + with open(self.getBuildArtifact("stdin.txt"), "w") as f: + f.write("STDIN CONTENT") + + process = target.Launch(info, error) + self.assertSuccess(error) + self.assertEqual(process.GetState(), lldb.eStateExited) + + with open(self.getBuildArtifact("stdout.txt")) as f: + self.assertEqual(f.read(), "STDOUT CONTENT") + with open(self.getBuildArtifact("stderr.txt")) as f: + self.assertEqual(f.read(), "STDERR CONTENT") + with open(self.getBuildArtifact("state.log")) as s: + state = json.load(s) + self.assertEqual(state["stdin"], "STDIN CONTENT") def test_bad_emulator_path(self): self.set_emulator_setting("emulator-path", diff --git a/lldb/test/API/qemu/qemu.py b/lldb/test/API/qemu/qemu.py --- a/lldb/test/API/qemu/qemu.py +++ b/lldb/test/API/qemu/qemu.py @@ -1,36 +1,63 @@ -from textwrap import dedent import argparse import socket import json +import sys import use_lldb_suite from lldbsuite.test.gdbclientutils import * +_description = """\ +Implements a fake qemu for testing purposes. The executable program +is not actually run. Instead a very basic mock process is presented +to lldb. This allows us to check the invocation parameters. + +The behavior of the emulated "process" is controlled via its command line +arguments, which should take the form of key:value pairs. Currently supported +actions are: +- dump: Dump the state of the emulator as a json dictionary. specifies + the target filename. +- stdout: Write to program stdout. +- stderr: Write to program stderr. +- stdin: Read a line from stdin and store it in the emulator state. + specifies the dictionary key. +""" + class MyResponder(MockGDBServerResponder): + def __init__(self, state): + super().__init__() + self._state = state + def cont(self): + for a in self._state["args"]: + action, data = a.split(":", 1) + if action == "dump": + with open(data, "w") as f: + json.dump(self._state, f) + elif action == "stdout": + sys.stdout.write(data) + elif action == "stderr": + sys.stderr.write(data) + elif action == "stdin": + self._state[data] = sys.stdin.readline() + else: + print("Unknown action: %r\n" % a) + return "X01" return "W47" class FakeEmulator(MockGDBServer): - def __init__(self, addr): + def __init__(self, addr, state): super().__init__(UnixServerSocket(addr)) - self.responder = MyResponder() + self.responder = MyResponder(state) def main(): - parser = argparse.ArgumentParser(description=dedent("""\ - Implements a fake qemu for testing purposes. The executable program - is not actually run. Instead a very basic mock process is presented - to lldb. The emulated program must accept at least one argument. - This should be a path where the emulator will dump its state. This - allows us to check the invocation parameters. - """)) + parser = argparse.ArgumentParser(description=_description, + formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument('-g', metavar="unix-socket", required=True) parser.add_argument('program', help="The program to 'emulate'.") - parser.add_argument('state_file', help="Where to dump the emulator state.") - parsed, rest = parser.parse_known_args() - with open(parsed.state_file, "w") as f: - json.dump({"program":parsed.program, "rest":rest}, f) + parser.add_argument("args", nargs=argparse.REMAINDER) + args = parser.parse_args() - emulator = FakeEmulator(parsed.g) + emulator = FakeEmulator(args.g, vars(args)) emulator.run() if __name__ == "__main__":