Skip to content

Commit 441587a

Browse files
authored
[lldb-dap] Add stdio redirection for integrated and external terminals (#161089)
This patch extends stdio redirection support to integrated and external terminals. Currently, these cases are not covered by the standard logic because `attach` is used instead of `launch`. To be honest, `runInTerminal` in [VSCode](https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/debug/node/terminals.ts#L188) request supports `>` and `<` for redirection, but not `2>`. We could use the `argsCanBeInterpretedByShell` option to use full power of shell, however it requieres proper escaping of arguments on lldb-dap side. So, I think it will be better to have the only one option to control stdio redirection that works consistently across all console modes. Also, fixed a small typo in a comparison that was leading to out-of-bound access, and added `null` as a possible value for `stdio` array in package.json.
1 parent b626282 commit 441587a

File tree

8 files changed

+140
-7
lines changed

8 files changed

+140
-7
lines changed

lldb/test/API/tools/lldb-dap/launch/TestDAP_launch.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -632,7 +632,27 @@ def test_stdio_redirection(self):
632632
program = self.getBuildArtifact("a.out")
633633

634634
with tempfile.NamedTemporaryFile("rt") as f:
635-
self.launch(program, stdio=[None, f.name, None])
635+
self.launch(program, stdio=[None, f.name])
636+
self.continue_to_exit()
637+
lines = f.readlines()
638+
self.assertIn(
639+
program, lines[0], "make sure program path is in first argument"
640+
)
641+
642+
@skipIfAsan
643+
@skipIfWindows
644+
@skipIf(oslist=["linux"], archs=no_match(["x86_64"]))
645+
def test_stdio_redirection_and_console(self):
646+
"""
647+
Test stdio redirection and console.
648+
"""
649+
self.build_and_create_debug_adapter()
650+
program = self.getBuildArtifact("a.out")
651+
652+
with tempfile.NamedTemporaryFile("rt") as f:
653+
self.launch(
654+
program, console="integratedTerminal", stdio=[None, f.name, None]
655+
)
636656
self.continue_to_exit()
637657
lines = f.readlines()
638658
self.assertIn(

lldb/tools/lldb-dap/Handler/RequestHandler.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ SetupIORedirection(const std::vector<std::optional<std::string>> &stdio,
5757
size_t n = std::max(stdio.size(), static_cast<size_t>(3));
5858
for (size_t i = 0; i < n; i++) {
5959
std::optional<std::string> path;
60-
if (stdio.size() < i)
60+
if (stdio.size() <= i)
6161
path = stdio.back();
6262
else
6363
path = stdio[i];
@@ -107,7 +107,7 @@ RunInTerminal(DAP &dap, const protocol::LaunchRequestArguments &arguments) {
107107

108108
llvm::json::Object reverse_request = CreateRunInTerminalReverseRequest(
109109
arguments.configuration.program, arguments.args, arguments.env,
110-
arguments.cwd, comm_file.m_path, debugger_pid,
110+
arguments.cwd, comm_file.m_path, debugger_pid, arguments.stdio,
111111
arguments.console == protocol::eConsoleExternalTerminal);
112112
dap.SendReverseRequest<LogFailureResponseHandler>("runInTerminal",
113113
std::move(reverse_request));

lldb/tools/lldb-dap/JSONUtils.cpp

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -866,7 +866,8 @@ llvm::json::Value CreateCompileUnit(lldb::SBCompileUnit &unit) {
866866
llvm::json::Object CreateRunInTerminalReverseRequest(
867867
llvm::StringRef program, const std::vector<std::string> &args,
868868
const llvm::StringMap<std::string> &env, llvm::StringRef cwd,
869-
llvm::StringRef comm_file, lldb::pid_t debugger_pid, bool external) {
869+
llvm::StringRef comm_file, lldb::pid_t debugger_pid,
870+
const std::vector<std::optional<std::string>> &stdio, bool external) {
870871
llvm::json::Object run_in_terminal_args;
871872
if (external) {
872873
// This indicates the IDE to open an external terminal window.
@@ -885,6 +886,18 @@ llvm::json::Object CreateRunInTerminalReverseRequest(
885886
}
886887
req_args.push_back("--launch-target");
887888
req_args.push_back(program.str());
889+
if (!stdio.empty()) {
890+
req_args.push_back("--stdio");
891+
std::stringstream ss;
892+
for (const std::optional<std::string> &file : stdio) {
893+
if (file)
894+
ss << *file;
895+
ss << ":";
896+
}
897+
std::string files = ss.str();
898+
files.pop_back();
899+
req_args.push_back(std::move(files));
900+
}
888901
req_args.insert(req_args.end(), args.begin(), args.end());
889902
run_in_terminal_args.try_emplace("args", req_args);
890903

lldb/tools/lldb-dap/JSONUtils.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,10 @@ llvm::json::Value CreateCompileUnit(lldb::SBCompileUnit &unit);
388388
/// launcher uses it on Linux tell the kernel that it should allow the
389389
/// debugger process to attach.
390390
///
391+
/// \param[in] stdio
392+
/// An array of file paths for redirecting the program's standard IO
393+
/// streams.
394+
///
391395
/// \param[in] external
392396
/// If set to true, the program will run in an external terminal window
393397
/// instead of IDE's integrated terminal.
@@ -398,7 +402,8 @@ llvm::json::Value CreateCompileUnit(lldb::SBCompileUnit &unit);
398402
llvm::json::Object CreateRunInTerminalReverseRequest(
399403
llvm::StringRef program, const std::vector<std::string> &args,
400404
const llvm::StringMap<std::string> &env, llvm::StringRef cwd,
401-
llvm::StringRef comm_file, lldb::pid_t debugger_pid, bool external);
405+
llvm::StringRef comm_file, lldb::pid_t debugger_pid,
406+
const std::vector<std::optional<std::string>> &stdio, bool external);
402407

403408
/// Create a "Terminated" JSON object that contains statistics
404409
///

lldb/tools/lldb-dap/Options.td

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ def debugger_pid: S<"debugger-pid">,
4747
HelpText<"The PID of the lldb-dap instance that sent the launchInTerminal "
4848
"request when using --launch-target.">;
4949

50+
def stdio: S<"stdio">,
51+
MetaVarName<"<stdin:stdout:stderr:...>">,
52+
HelpText<"An array of file paths for redirecting the program's standard IO "
53+
"streams. A colon-separated list of entries. Empty value means no "
54+
"redirection.">;
55+
5056
def repl_mode
5157
: S<"repl-mode">,
5258
MetaVarName<"<mode>">,

lldb/tools/lldb-dap/Protocol/ProtocolRequests.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ struct LaunchRequestArguments {
300300
/// terminal or external terminal.
301301
Console console = eConsoleInternal;
302302

303+
/// An array of file paths for redirecting the program's standard IO streams.
303304
std::vector<std::optional<std::string>> stdio;
304305

305306
/// @}

lldb/tools/lldb-dap/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -626,7 +626,10 @@
626626
"stdio": {
627627
"type": "array",
628628
"items": {
629-
"type": "string"
629+
"type": [
630+
"string",
631+
"null"
632+
]
630633
},
631634
"description": "The stdio property specifies the redirection targets for the debuggee's stdio streams. A null value redirects a stream to the default debug terminal. String can be a path to file, named pipe or TTY device. If less than three values are provided, the list will be padded with the last value. Specifying more than three values will create additional file descriptors (4, 5, etc.).",
632635
"default": []

lldb/tools/lldb-dap/tool/lldb-dap.cpp

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include "lldb/API/SBStream.h"
1717
#include "lldb/Host/Config.h"
1818
#include "lldb/Host/File.h"
19+
#include "lldb/Host/FileSystem.h"
1920
#include "lldb/Host/MainLoop.h"
2021
#include "lldb/Host/MainLoopBase.h"
2122
#include "lldb/Host/MemoryMonitor.h"
@@ -24,7 +25,9 @@
2425
#include "lldb/Utility/UriParser.h"
2526
#include "lldb/lldb-forward.h"
2627
#include "llvm/ADT/ArrayRef.h"
28+
#include "llvm/ADT/DenseMap.h"
2729
#include "llvm/ADT/ScopeExit.h"
30+
#include "llvm/ADT/SmallVector.h"
2831
#include "llvm/ADT/StringExtras.h"
2932
#include "llvm/ADT/StringRef.h"
3033
#include "llvm/Option/Arg.h"
@@ -42,8 +45,10 @@
4245
#include "llvm/Support/WithColor.h"
4346
#include "llvm/Support/raw_ostream.h"
4447
#include <condition_variable>
48+
#include <cstddef>
4549
#include <cstdio>
4650
#include <cstdlib>
51+
#include <exception>
4752
#include <fcntl.h>
4853
#include <map>
4954
#include <memory>
@@ -143,6 +148,74 @@ static void PrintVersion() {
143148
llvm::outs() << "liblldb: " << lldb::SBDebugger::GetVersionString() << '\n';
144149
}
145150

151+
#if not defined(_WIN32)
152+
struct FDGroup {
153+
int GetFlags() const {
154+
if (read && write)
155+
return O_NOCTTY | O_CREAT | O_RDWR;
156+
if (read)
157+
return O_NOCTTY | O_RDONLY;
158+
return O_NOCTTY | O_CREAT | O_WRONLY | O_TRUNC;
159+
}
160+
161+
std::vector<int> fds;
162+
bool read = false;
163+
bool write = false;
164+
};
165+
166+
static llvm::Error RedirectToFile(const FDGroup &fdg, llvm::StringRef file) {
167+
if (!fdg.read && !fdg.write)
168+
return llvm::Error::success();
169+
int target_fd = lldb_private::FileSystem::Instance().Open(
170+
file.str().c_str(), fdg.GetFlags(), 0666);
171+
if (target_fd == -1)
172+
return llvm::errorCodeToError(
173+
std::error_code(errno, std::generic_category()));
174+
for (int fd : fdg.fds) {
175+
if (target_fd == fd)
176+
continue;
177+
if (::dup2(target_fd, fd) == -1)
178+
return llvm::errorCodeToError(
179+
std::error_code(errno, std::generic_category()));
180+
}
181+
::close(target_fd);
182+
return llvm::Error::success();
183+
}
184+
185+
static llvm::Error
186+
SetupIORedirection(const llvm::SmallVectorImpl<llvm::StringRef> &files) {
187+
llvm::SmallDenseMap<llvm::StringRef, FDGroup> groups;
188+
for (size_t i = 0; i < files.size(); i++) {
189+
if (files[i].empty())
190+
continue;
191+
auto group = groups.find(files[i]);
192+
if (group == groups.end())
193+
group = groups.insert({files[i], {{static_cast<int>(i)}}}).first;
194+
else
195+
group->second.fds.push_back(i);
196+
switch (i) {
197+
case 0:
198+
group->second.read = true;
199+
break;
200+
case 1:
201+
case 2:
202+
group->second.write = true;
203+
break;
204+
default:
205+
group->second.read = true;
206+
group->second.write = true;
207+
break;
208+
}
209+
}
210+
for (const auto &[file, group] : groups) {
211+
if (llvm::Error err = RedirectToFile(group, file))
212+
return llvm::createStringError(
213+
llvm::formatv("{0}: {1}", file, llvm::toString(std::move(err))));
214+
}
215+
return llvm::Error::success();
216+
}
217+
#endif
218+
146219
// If --launch-target is provided, this instance of lldb-dap becomes a
147220
// runInTerminal launcher. It will ultimately launch the program specified in
148221
// the --launch-target argument, which is the original program the user wanted
@@ -165,6 +238,7 @@ static void PrintVersion() {
165238
static llvm::Error LaunchRunInTerminalTarget(llvm::opt::Arg &target_arg,
166239
llvm::StringRef comm_file,
167240
lldb::pid_t debugger_pid,
241+
llvm::StringRef stdio,
168242
char *argv[]) {
169243
#if defined(_WIN32)
170244
return llvm::createStringError(
@@ -179,6 +253,16 @@ static llvm::Error LaunchRunInTerminalTarget(llvm::opt::Arg &target_arg,
179253
(void)prctl(PR_SET_PTRACER, debugger_pid, 0, 0, 0);
180254
#endif
181255

256+
lldb_private::FileSystem::Initialize();
257+
if (!stdio.empty()) {
258+
llvm::SmallVector<llvm::StringRef, 3> files;
259+
stdio.split(files, ':');
260+
while (files.size() < 3)
261+
files.push_back(files.back());
262+
if (llvm::Error err = SetupIORedirection(files))
263+
return err;
264+
}
265+
182266
RunInTerminalLauncherCommChannel comm_channel(comm_file);
183267
if (llvm::Error err = comm_channel.NotifyPid())
184268
return err;
@@ -484,9 +568,10 @@ int main(int argc, char *argv[]) {
484568
break;
485569
}
486570
}
571+
llvm::StringRef stdio = input_args.getLastArgValue(OPT_stdio);
487572
if (llvm::Error err =
488573
LaunchRunInTerminalTarget(*target_arg, comm_file->getValue(), pid,
489-
argv + target_args_pos)) {
574+
stdio, argv + target_args_pos)) {
490575
llvm::errs() << llvm::toString(std::move(err)) << '\n';
491576
return EXIT_FAILURE;
492577
}

0 commit comments

Comments
 (0)