Depends on D93874.
runInTerminal was using --wait-for, but it was some problems because it uses process polling looking for a single instance of the debuggee:
- it gets to know of the target late, which renders breakpoints in the main function almost impossible
- polling might fail if there are already other processes with the same name
- polling might also fail on some linux machine, as it's implemented with the ps command, and the ps command's args and output are not standard everywhere
As a better way to implement this so that it works well on Darwin and Linux, I'm using now the following process:
- lldb-vscode notices the runInTerminal, so it spawns lldb-vscode with a special flag --launch-target <target>. This flags tells lldb-vscode to wait to be attached and then it execs the target program. I'm using lldb-vscode itself to do this, because it makes finding the launcher program easier. Also no CMAKE INSTALL scripts are needed.
- Besides this, the debugger creates a temporary FIFO file where the launcher program will write its pid to. That way the debugger will be sure of which program to attach.
- Once attach happend, the debugger creates a second temporary file to notify the launcher program that it has been attached, so that it can then exec. I'm using this instead of using a signal or a similar mechanism because I don't want the launcher program to wait indefinitely to be attached in case the debugger crashed. That would pollute the process list with a lot of hanging processes. Instead, I'm setting a 20 seconds timeout (that's an overkill) and the launcher program seeks in intervals the second tepmorary file.
Some notes:
- I preferred not to use sockets because it requires a lot of code and I only need a pid. It would also require a lot of code when windows support is implemented.
- I didn't add Windows support, as I don't have a windows machine, but adding support for it should be easy, as the FIFO file can be implemented with a named pipe, which is standard on Windows and works pretty much the same way.
The existing test which didn't pass on Linux, now passes.
Differential Revision: https://reviews.llvm.org/D93951
trace=False, initCommands=None, preRunCommands=None,
stopCommands=None, exitCommands=None, terminateCommands=None,
sourcePath=None, debuggerRoot=None, launchCommands=None,
- sourceMap=None, disconnectAutomatically=True, runInTerminal=False):
+ sourceMap=None, disconnectAutomatically=True, runInTerminal=False,
+ expectFailure=False):
'''Sending launch request to vscode
'''
debuggerRoot=debuggerRoot,
launchCommands=launchCommands,
sourceMap=sourceMap,
- runInTerminal=runInTerminal)
+ runInTerminal=runInTerminal,
+ expectFailure=expectFailure)
+
+ if expectFailure:
+ return response
+
if not (response and response['success']):
self.assertTrue(response['success'],
'launch failed (%s)' % (response['message']))
# attached a runInTerminal process to finish initialization.
if runInTerminal:
self.vscode.request_configurationDone()
+ return response
def build_and_launch(self, program, args=None, cwd=None, env=None,
self.build_and_create_debug_adaptor()
self.assertTrue(os.path.exists(program), 'executable must exist')
- self.launch(program, args, cwd, env, stopOnEntry, disableASLR,
+ return self.launch(program, args, cwd, env, stopOnEntry, disableASLR,
disableSTDIO, shellExpandArguments, trace,
initCommands, preRunCommands, stopCommands, exitCommands,
terminateCommands, sourcePath, debuggerRoot, runInTerminal=runInTerminal)
stopCommands=None, exitCommands=None,
terminateCommands=None ,sourcePath=None,
debuggerRoot=None, launchCommands=None, sourceMap=None,
- runInTerminal=False):
+ runInTerminal=False, expectFailure=False):
args_dict = {
'program': program
}
}
response = self.send_recv(command_dict)
- # Wait for a 'process' and 'initialized' event in any order
- self.wait_for_event(filter=['process', 'initialized'])
- self.wait_for_event(filter=['process', 'initialized'])
+ if not expectFailure:
+ # Wait for a 'process' and 'initialized' event in any order
+ self.wait_for_event(filter=['process', 'initialized'])
+ self.wait_for_event(filter=['process', 'initialized'])
return response
def request_next(self, threadId):
import lldbvscode_testcase
import time
import os
+import subprocess
+import shutil
+import json
+from threading import Thread
class TestVSCode_runInTerminal(lldbvscode_testcase.VSCodeTestCaseBase):
mydir = TestBase.compute_mydir(__file__)
- @skipUnlessDarwin
+ def readPidMessage(self, fifo_file):
+ with open(fifo_file, "r") as file:
+ self.assertIn("pid", file.readline())
+
+ def sendDidAttachMessage(self, fifo_file):
+ with open(fifo_file, "w") as file:
+ file.write(json.dumps({"kind": "didAttach"}) + "\n")
+
+ def readErrorMessage(self, fifo_file):
+ with open(fifo_file, "r") as file:
+ return file.readline()
+
+ @skipIfWindows
@skipIfRemote
def test_runInTerminal(self):
'''
Tests the "runInTerminal" reverse request. It makes sure that the IDE can
launch the inferior with the correct environment variables and arguments.
'''
+ if "debug" in str(os.environ["LLDBVSCODE_EXEC"]).lower():
+ # We skip this test for debug builds because it takes too long parsing lldb's own
+ # debug info. Release builds are fine.
+ # Checking this environment variable seems to be a decent proxy for a quick
+ # detection
+ return
program = self.getBuildArtifact("a.out")
source = 'main.c'
- self.build_and_launch(program, stopOnEntry=True, runInTerminal=True, args=["foobar"], env=["FOO=bar"])
+ self.build_and_launch(
+ program, stopOnEntry=True, runInTerminal=True, args=["foobar"],
+ env=["FOO=bar"])
+
breakpoint_line = line_number(source, '// breakpoint')
self.set_source_breakpoints(source, [breakpoint_line])
# We verify we were able to set the environment
env = self.vscode.request_evaluate('foo')['body']['result']
self.assertIn('bar', env)
+
+ @skipIfWindows
+ @skipIfRemote
+ def test_runInTerminalInvalidTarget(self):
+ self.build_and_create_debug_adaptor()
+ response = self.launch(
+ "INVALIDPROGRAM", stopOnEntry=True, runInTerminal=True, args=["foobar"], env=["FOO=bar"], expectFailure=True)
+ self.assertFalse(response['success'])
+ self.assertIn("Could not create a target for a program 'INVALIDPROGRAM': unable to find executable",
+ response['message'])
+
+ @skipIfWindows
+ @skipIfRemote
+ def test_missingArgInRunInTerminalLauncher(self):
+ proc = subprocess.run([self.lldbVSCodeExec, "--launch-target", "INVALIDPROGRAM"],
+ capture_output=True, universal_newlines=True)
+ self.assertTrue(proc.returncode != 0)
+ self.assertIn('"--launch-target" requires "--comm-file" to be specified', proc.stderr)
+
+ @skipIfWindows
+ @skipIfRemote
+ def test_FakeAttachedRunInTerminalLauncherWithInvalidProgram(self):
+ comm_file = os.path.join(self.getBuildDir(), "comm-file")
+ os.mkfifo(comm_file)
+
+ proc = subprocess.Popen(
+ [self.lldbVSCodeExec, "--comm-file", comm_file, "--launch-target", "INVALIDPROGRAM"],
+ universal_newlines=True, stderr=subprocess.PIPE)
+
+ self.readPidMessage(comm_file)
+ self.sendDidAttachMessage(comm_file)
+ self.assertIn("No such file or directory", self.readErrorMessage(comm_file))
+
+ _, stderr = proc.communicate()
+ self.assertIn("No such file or directory", stderr)
+
+ @skipIfWindows
+ @skipIfRemote
+ def test_FakeAttachedRunInTerminalLauncherWithValidProgram(self):
+ comm_file = os.path.join(self.getBuildDir(), "comm-file")
+ os.mkfifo(comm_file)
+
+ proc = subprocess.Popen(
+ [self.lldbVSCodeExec, "--comm-file", comm_file, "--launch-target", "echo", "foo"],
+ universal_newlines=True, stdout=subprocess.PIPE)
+
+ self.readPidMessage(comm_file)
+ self.sendDidAttachMessage(comm_file)
+
+ stdout, _ = proc.communicate()
+ self.assertIn("foo", stdout)
+
+ @skipIfWindows
+ @skipIfRemote
+ def test_FakeAttachedRunInTerminalLauncherAndCheckEnvironment(self):
+ comm_file = os.path.join(self.getBuildDir(), "comm-file")
+ os.mkfifo(comm_file)
+
+ proc = subprocess.Popen(
+ [self.lldbVSCodeExec, "--comm-file", comm_file, "--launch-target", "env"],
+ universal_newlines=True, stdout=subprocess.PIPE,
+ env={**os.environ, "FOO": "BAR"})
+
+ self.readPidMessage(comm_file)
+ self.sendDidAttachMessage(comm_file)
+
+ stdout, _ = proc.communicate()
+ self.assertIn("FOO=BAR", stdout)
+
+ @skipIfWindows
+ @skipIfRemote
+ def test_NonAttachedRunInTerminalLauncher(self):
+ comm_file = os.path.join(self.getBuildDir(), "comm-file")
+ os.mkfifo(comm_file)
+
+ proc = subprocess.Popen(
+ [self.lldbVSCodeExec, "--comm-file", comm_file, "--launch-target", "echo", "foo"],
+ universal_newlines=True, stderr=subprocess.PIPE,
+ env={**os.environ, "LLDB_VSCODE_RIT_TIMEOUT_IN_MS": "1000"})
+
+ self.readPidMessage(comm_file)
+
+ _, stderr = proc.communicate()
+ self.assertIn("Timed out trying to get messages from the debug adaptor", stderr)
lldb-vscode.cpp
BreakpointBase.cpp
ExceptionBreakpoint.cpp
+ FifoFiles.cpp
FunctionBreakpoint.cpp
IOStream.cpp
JSONUtils.cpp
LLDBUtils.cpp
+ RunInTerminal.cpp
SourceBreakpoint.cpp
VSCode.cpp
--- /dev/null
+//===-- FifoFiles.cpp -------------------------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#if !defined(WIN32)
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+#endif
+
+#include <chrono>
+#include <fstream>
+#include <future>
+#include <thread>
+
+#include "llvm/Support/FileSystem.h"
+
+#include "lldb/lldb-defines.h"
+
+#include "FifoFiles.h"
+
+using namespace llvm;
+
+namespace lldb_vscode {
+
+FifoFile::FifoFile(StringRef path) : m_path(path) {}
+
+FifoFile::~FifoFile() {
+#if !defined(WIN32)
+ unlink(m_path.c_str());
+#endif
+};
+
+Expected<std::shared_ptr<FifoFile>> CreateFifoFile(StringRef path) {
+#if defined(WIN32)
+ return createStringError(inconvertibleErrorCode(), "Unimplemented");
+#else
+ if (int err = mkfifo(path.data(), 0600))
+ return createStringError(std::error_code(err, std::generic_category()),
+ "Couldn't create fifo file: %s", path.data());
+ return std::make_shared<FifoFile>(path);
+#endif
+}
+
+FifoFileIO::FifoFileIO(StringRef fifo_file, StringRef other_endpoint_name)
+ : m_fifo_file(fifo_file), m_other_endpoint_name(other_endpoint_name) {}
+
+Expected<json::Value> FifoFileIO::ReadJSON(std::chrono::milliseconds timeout) {
+ // We use a pointer for this future, because otherwise its normal destructor
+ // would wait for the getline to end, rendering the timeout useless.
+ Optional<std::string> line;
+ std::future<void> *future =
+ new std::future<void>(std::async(std::launch::async, [&]() {
+ std::ifstream reader(m_fifo_file, std::ifstream::in);
+ std::string buffer;
+ std::getline(reader, buffer);
+ if (!buffer.empty())
+ line = buffer;
+ }));
+ if (future->wait_for(timeout) == std::future_status::timeout ||
+ !line.hasValue())
+ return createStringError(inconvertibleErrorCode(),
+ "Timed out trying to get messages from the " +
+ m_other_endpoint_name);
+ delete future;
+ return json::parse(*line);
+}
+
+Error FifoFileIO::SendJSON(const json::Value &json,
+ std::chrono::milliseconds timeout) {
+ bool done = false;
+ std::future<void> *future =
+ new std::future<void>(std::async(std::launch::async, [&]() {
+ std::ofstream writer(m_fifo_file, std::ofstream::out);
+ writer << JSONToString(json) << std::endl;
+ done = true;
+ }));
+ if (future->wait_for(timeout) == std::future_status::timeout || !done) {
+ return createStringError(inconvertibleErrorCode(),
+ "Timed out trying to send messages to the " +
+ m_other_endpoint_name);
+ }
+ delete future;
+ return Error::success();
+}
+
+} // namespace lldb_vscode
--- /dev/null
+//===-- FifoFiles.h ---------------------------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_TOOLS_LLDB_VSCODE_FIFOFILES_H
+#define LLDB_TOOLS_LLDB_VSCODE_FIFOFILES_H
+
+#include "llvm/Support/Error.h"
+
+#include "JSONUtils.h"
+
+namespace lldb_vscode {
+
+/// Struct that controls the life of a fifo file in the filesystem.
+///
+/// The file is destroyed when the destructor is invoked.
+struct FifoFile {
+ FifoFile(llvm::StringRef path);
+
+ ~FifoFile();
+
+ std::string m_path;
+};
+
+/// Create a fifo file in the filesystem.
+///
+/// \param[in] path
+/// The path for the fifo file.
+///
+/// \return
+/// A \a std::shared_ptr<FifoFile> if the file could be created, or an
+/// \a llvm::Error in case of failures.
+llvm::Expected<std::shared_ptr<FifoFile>> CreateFifoFile(llvm::StringRef path);
+
+class FifoFileIO {
+public:
+ /// \param[in] fifo_file
+ /// The path to an input fifo file that exists in the file system.
+ ///
+ /// \param[in] other_endpoint_name
+ /// A human readable name for the other endpoint that will communicate
+ /// using this file. This is used for error messages.
+ FifoFileIO(llvm::StringRef fifo_file, llvm::StringRef other_endpoint_name);
+
+ /// Read the next JSON object from the underlying input fifo file.
+ ///
+ /// The JSON object is expected to be a single line delimited with \a
+ /// std::endl.
+ ///
+ /// \return
+ /// An \a llvm::Error object indicating the success or failure of this
+ /// operation. Failures arise if the timeout is hit, the next line of text
+ /// from the fifo file is not a valid JSON object, or is it impossible to
+ /// read from the file.
+ llvm::Expected<llvm::json::Value> ReadJSON(std::chrono::milliseconds timeout);
+
+ /// Serialize a JSON object and write it to the underlying output fifo file.
+ ///
+ /// \param[in] json
+ /// The JSON object to send. It will be printed as a single line delimited
+ /// with \a std::endl.
+ ///
+ /// \param[in] timeout
+ /// A timeout for how long we should until for the data to be consumed.
+ ///
+ /// \return
+ /// An \a llvm::Error object indicating whether the data was consumed by
+ /// a reader or not.
+ llvm::Error SendJSON(
+ const llvm::json::Value &json,
+ std::chrono::milliseconds timeout = std::chrono::milliseconds(20000));
+
+private:
+ std::string m_fifo_file;
+ std::string m_other_endpoint_name;
+};
+
+} // namespace lldb_vscode
+
+#endif // LLDB_TOOLS_LLDB_VSCODE_FIFOFILES_H
/// See
/// https://microsoft.github.io/debug-adapter-protocol/specification#Reverse_Requests_RunInTerminal
llvm::json::Object
-CreateRunInTerminalReverseRequest(const llvm::json::Object &launch_request) {
+CreateRunInTerminalReverseRequest(const llvm::json::Object &launch_request,
+ llvm::StringRef debug_adaptor_path,
+ llvm::StringRef comm_file) {
llvm::json::Object reverse_request;
reverse_request.try_emplace("type", "request");
reverse_request.try_emplace("command", "runInTerminal");
run_in_terminal_args.try_emplace("kind", "integrated");
auto launch_request_arguments = launch_request.getObject("arguments");
- std::vector<std::string> args = GetStrings(launch_request_arguments, "args");
// The program path must be the first entry in the "args" field
- args.insert(args.begin(),
- GetString(launch_request_arguments, "program").str());
+ std::vector<std::string> args = {
+ debug_adaptor_path.str(), "--comm-file", comm_file.str(),
+ "--launch-target", GetString(launch_request_arguments, "program").str()};
+ std::vector<std::string> target_args =
+ GetStrings(launch_request_arguments, "args");
+ args.insert(args.end(), target_args.begin(), target_args.end());
run_in_terminal_args.try_emplace("args", args);
const auto cwd = GetString(launch_request_arguments, "cwd");
return reverse_request;
}
+std::string JSONToString(const llvm::json::Value &json) {
+ std::string data;
+ llvm::raw_string_ostream os(data);
+ os << json;
+ os.flush();
+ return data;
+}
+
} // namespace lldb_vscode
/// The original launch_request object whose fields are used to construct
/// the reverse request object.
///
+/// \param[in] debug_adaptor_path
+/// Path to the current debug adaptor. It will be used to delegate the
+/// launch of the target.
+///
+/// \param[in] comm_file
+/// The fifo file used to communicate the with the target launcher.
+///
/// \return
/// A "runInTerminal" JSON object that follows the specification outlined by
/// Microsoft.
llvm::json::Object
-CreateRunInTerminalReverseRequest(const llvm::json::Object &launch_request);
+CreateRunInTerminalReverseRequest(const llvm::json::Object &launch_request,
+ llvm::StringRef debug_adaptor_path,
+ llvm::StringRef comm_file);
+
+/// Convert a given JSON object to a string.
+std::string JSONToString(const llvm::json::Value &json);
} // namespace lldb_vscode
def: Separate<["-"], "p">,
Alias<port>,
HelpText<"Alias for --port">;
+
+def launch_target: Separate<["--", "-"], "launch-target">,
+ MetaVarName<"<target>">,
+ HelpText<"Launch a target for the launchInTerminal request. Any argument "
+ "provided after this one will be passed to the target. The parameter "
+ "--comm-files-prefix must also be specified.">;
+
+def comm_file: Separate<["--", "-"], "comm-file">,
+ MetaVarName<"<file>">,
+ HelpText<"The fifo file used to communicate the with the debug adaptor"
+ "when using --launch-target.">;
--- /dev/null
+//===-- RunInTerminal.cpp ---------------------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#if !defined(WIN32)
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+#endif
+
+#include <chrono>
+#include <fstream>
+#include <future>
+#include <thread>
+
+#include "llvm/Support/FileSystem.h"
+
+#include "lldb/lldb-defines.h"
+
+#include "RunInTerminal.h"
+
+using namespace llvm;
+
+namespace lldb_vscode {
+
+const RunInTerminalMessagePid *RunInTerminalMessage::GetAsPidMessage() const {
+ return static_cast<const RunInTerminalMessagePid *>(this);
+}
+
+const RunInTerminalMessageError *
+RunInTerminalMessage::GetAsErrorMessage() const {
+ return static_cast<const RunInTerminalMessageError *>(this);
+}
+
+RunInTerminalMessage::RunInTerminalMessage(RunInTerminalMessageKind kind)
+ : kind(kind) {}
+
+RunInTerminalMessagePid::RunInTerminalMessagePid(lldb::pid_t pid)
+ : RunInTerminalMessage(eRunInTerminalMessageKindPID), pid(pid) {}
+
+json::Value RunInTerminalMessagePid::ToJSON() const {
+ return json::Object{{"kind", "pid"}, {"pid", static_cast<int64_t>(pid)}};
+}
+
+RunInTerminalMessageError::RunInTerminalMessageError(StringRef error)
+ : RunInTerminalMessage(eRunInTerminalMessageKindError), error(error) {}
+
+json::Value RunInTerminalMessageError::ToJSON() const {
+ return json::Object{{"kind", "error"}, {"value", error}};
+}
+
+RunInTerminalMessageDidAttach::RunInTerminalMessageDidAttach()
+ : RunInTerminalMessage(eRunInTerminalMessageKindDidAttach) {}
+
+json::Value RunInTerminalMessageDidAttach::ToJSON() const {
+ return json::Object{{"kind", "didAttach"}};
+}
+
+static Expected<RunInTerminalMessageUP>
+ParseJSONMessage(const json::Value &json) {
+ if (const json::Object *obj = json.getAsObject()) {
+ if (Optional<StringRef> kind = obj->getString("kind")) {
+ if (*kind == "pid") {
+ if (Optional<int64_t> pid = obj->getInteger("pid"))
+ return std::make_unique<RunInTerminalMessagePid>(
+ static_cast<lldb::pid_t>(*pid));
+ } else if (*kind == "error") {
+ if (Optional<StringRef> error = obj->getString("error"))
+ return std::make_unique<RunInTerminalMessageError>(*error);
+ } else if (*kind == "didAttach") {
+ return std::make_unique<RunInTerminalMessageDidAttach>();
+ }
+ }
+ }
+
+ return createStringError(inconvertibleErrorCode(),
+ "Incorrect JSON message: " + JSONToString(json));
+}
+
+static Expected<RunInTerminalMessageUP>
+GetNextMessage(FifoFileIO &io, std::chrono::milliseconds timeout) {
+ if (Expected<json::Value> json = io.ReadJSON(timeout))
+ return ParseJSONMessage(*json);
+ else
+ return json.takeError();
+}
+
+static Error ToError(const RunInTerminalMessage &message) {
+ if (message.kind == eRunInTerminalMessageKindError)
+ return createStringError(inconvertibleErrorCode(),
+ message.GetAsErrorMessage()->error);
+ return createStringError(inconvertibleErrorCode(),
+ "Unexpected JSON message: " +
+ JSONToString(message.ToJSON()));
+}
+
+RunInTerminalLauncherCommChannel::RunInTerminalLauncherCommChannel(
+ StringRef comm_file)
+ : m_io(comm_file, "debug adaptor") {}
+
+Error RunInTerminalLauncherCommChannel::WaitUntilDebugAdaptorAttaches(
+ std::chrono::milliseconds timeout) {
+ if (Expected<RunInTerminalMessageUP> message =
+ GetNextMessage(m_io, timeout)) {
+ if (message.get()->kind == eRunInTerminalMessageKindDidAttach)
+ return Error::success();
+ else
+ return ToError(*message.get());
+ } else
+ return message.takeError();
+}
+
+Error RunInTerminalLauncherCommChannel::NotifyPid() {
+ return m_io.SendJSON(RunInTerminalMessagePid(getpid()).ToJSON());
+}
+
+void RunInTerminalLauncherCommChannel::NotifyError(StringRef error) {
+ if (Error err = m_io.SendJSON(RunInTerminalMessageError(error).ToJSON(),
+ std::chrono::seconds(2)))
+ llvm::errs() << llvm::toString(std::move(err)) << "\n";
+}
+
+RunInTerminalDebugAdapterCommChannel::RunInTerminalDebugAdapterCommChannel(
+ StringRef comm_file)
+ : m_io(comm_file, "runInTerminal launcher") {}
+
+std::future<Error> RunInTerminalDebugAdapterCommChannel::NotifyDidAttach() {
+ return std::async(std::launch::async, [&]() {
+ return m_io.SendJSON(RunInTerminalMessageDidAttach().ToJSON());
+ });
+}
+
+Expected<lldb::pid_t> RunInTerminalDebugAdapterCommChannel::GetLauncherPid() {
+ if (Expected<RunInTerminalMessageUP> message =
+ GetNextMessage(m_io, std::chrono::seconds(20))) {
+ if (message.get()->kind == eRunInTerminalMessageKindPID)
+ return message.get()->GetAsPidMessage()->pid;
+ return ToError(*message.get());
+ } else {
+ return message.takeError();
+ }
+}
+
+std::string RunInTerminalDebugAdapterCommChannel::GetLauncherError() {
+ // We know there's been an error, so a small timeout is enough.
+ if (Expected<RunInTerminalMessageUP> message =
+ GetNextMessage(m_io, std::chrono::seconds(1)))
+ return toString(ToError(*message.get()));
+ else
+ return toString(message.takeError());
+}
+
+Expected<std::shared_ptr<FifoFile>> CreateRunInTerminalCommFile() {
+ SmallString<256> comm_file;
+ if (std::error_code EC = sys::fs::getPotentiallyUniqueTempFileName(
+ "lldb-vscode-run-in-terminal-comm", "", comm_file))
+ return createStringError(EC, "Error making unique file name for "
+ "runInTerminal communication files");
+
+ return CreateFifoFile(comm_file.str());
+}
+
+} // namespace lldb_vscode
--- /dev/null
+//===-- RunInTerminal.h ----------------------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_TOOLS_LLDB_VSCODE_RUNINTERMINAL_H
+#define LLDB_TOOLS_LLDB_VSCODE_RUNINTERMINAL_H
+
+#include "FifoFiles.h"
+
+#include <future>
+#include <thread>
+
+namespace lldb_vscode {
+
+enum RunInTerminalMessageKind {
+ eRunInTerminalMessageKindPID = 0,
+ eRunInTerminalMessageKindError,
+ eRunInTerminalMessageKindDidAttach,
+};
+
+struct RunInTerminalMessage;
+struct RunInTerminalMessagePid;
+struct RunInTerminalMessageError;
+struct RunInTerminalMessageDidAttach;
+
+struct RunInTerminalMessage {
+ RunInTerminalMessage(RunInTerminalMessageKind kind);
+
+ virtual ~RunInTerminalMessage() = default;
+
+ /// Serialize this object to JSON
+ virtual llvm::json::Value ToJSON() const = 0;
+
+ const RunInTerminalMessagePid *GetAsPidMessage() const;
+
+ const RunInTerminalMessageError *GetAsErrorMessage() const;
+
+ RunInTerminalMessageKind kind;
+};
+
+using RunInTerminalMessageUP = std::unique_ptr<RunInTerminalMessage>;
+
+struct RunInTerminalMessagePid : RunInTerminalMessage {
+ RunInTerminalMessagePid(lldb::pid_t pid);
+
+ llvm::json::Value ToJSON() const override;
+
+ lldb::pid_t pid;
+};
+
+struct RunInTerminalMessageError : RunInTerminalMessage {
+ RunInTerminalMessageError(llvm::StringRef error);
+
+ llvm::json::Value ToJSON() const override;
+
+ std::string error;
+};
+
+struct RunInTerminalMessageDidAttach : RunInTerminalMessage {
+ RunInTerminalMessageDidAttach();
+
+ llvm::json::Value ToJSON() const override;
+};
+
+class RunInTerminalLauncherCommChannel {
+public:
+ RunInTerminalLauncherCommChannel(llvm::StringRef comm_file);
+
+ /// Wait until the debug adaptor attaches.
+ ///
+ /// \param[in] timeout
+ /// How long to wait to be attached.
+ //
+ /// \return
+ /// An \a llvm::Error object in case of errors or if this operation times
+ /// out.
+ llvm::Error WaitUntilDebugAdaptorAttaches(std::chrono::milliseconds timeout);
+
+ /// Notify the debug adaptor this process' pid.
+ ///
+ /// \return
+ /// An \a llvm::Error object in case of errors or if this operation times
+ /// out.
+ llvm::Error NotifyPid();
+
+ /// Notify the debug adaptor that there's been an error.
+ void NotifyError(llvm::StringRef error);
+
+private:
+ FifoFileIO m_io;
+};
+
+class RunInTerminalDebugAdapterCommChannel {
+public:
+ RunInTerminalDebugAdapterCommChannel(llvm::StringRef comm_file);
+
+ /// Notify the runInTerminal launcher that it was attached.
+ ///
+ /// \return
+ /// A future indicated whether the runInTerminal launcher received the
+ /// message correctly or not.
+ std::future<llvm::Error> NotifyDidAttach();
+
+ /// Fetch the pid of the runInTerminal launcher.
+ ///
+ /// \return
+ /// An \a llvm::Error object in case of errors or if this operation times
+ /// out.
+ llvm::Expected<lldb::pid_t> GetLauncherPid();
+
+ /// Fetch any errors emitted by the runInTerminal launcher or return a
+ /// default error message if a certain timeout if reached.
+ std::string GetLauncherError();
+
+private:
+ FifoFileIO m_io;
+};
+
+/// Create a fifo file used to communicate the debug adaptor with
+/// the runInTerminal launcher.
+llvm::Expected<std::shared_ptr<FifoFile>> CreateRunInTerminalCommFile();
+
+} // namespace lldb_vscode
+
+#endif // LLDB_TOOLS_LLDB_VSCODE_RUNINTERMINAL_H
#include "ExceptionBreakpoint.h"
#include "FunctionBreakpoint.h"
#include "IOStream.h"
+#include "RunInTerminal.h"
#include "SourceBreakpoint.h"
#include "SourceReference.h"
};
struct VSCode {
+ std::string debug_adaptor_path;
InputStream input;
OutputStream output;
lldb::SBDebugger debugger;
bool is_attach;
uint32_t reverse_request_seq;
std::map<std::string, RequestCallback> request_handlers;
- std::condition_variable request_in_terminal_cv;
bool waiting_for_run_in_terminal;
// Keep track of the last stop thread index IDs as threads won't go away
// unless we send a "thread" event to indicate the thread exited.
break;
case lldb::eStateSuspended:
break;
- case lldb::eStateStopped: {
- if (g_vsc.waiting_for_run_in_terminal) {
- g_vsc.waiting_for_run_in_terminal = false;
- g_vsc.request_in_terminal_cv.notify_one();
- }
- }
+ case lldb::eStateStopped:
// Only report a stopped event if the process was not restarted.
if (!lldb::SBProcess::GetRestartedFromEvent(event)) {
SendStdOutStdErr(process);
g_vsc.SendJSON(llvm::json::Value(std::move(response)));
}
-void request_runInTerminal(const llvm::json::Object &launch_request,
- llvm::json::Object &launch_response) {
- // We have already created a target that has a valid "program" path to the
- // executable. We will attach to the next process whose name matches that
- // of the target's.
+llvm::Error request_runInTerminal(const llvm::json::Object &launch_request) {
g_vsc.is_attach = true;
lldb::SBAttachInfo attach_info;
- lldb::SBError error;
- attach_info.SetWaitForLaunch(true, /*async*/ true);
- g_vsc.target.Attach(attach_info, error);
- llvm::json::Object reverse_request =
- CreateRunInTerminalReverseRequest(launch_request);
+ llvm::Expected<std::shared_ptr<FifoFile>> comm_file_or_err =
+ CreateRunInTerminalCommFile();
+ if (!comm_file_or_err)
+ return comm_file_or_err.takeError();
+ FifoFile &comm_file = *comm_file_or_err.get();
+
+ RunInTerminalDebugAdapterCommChannel comm_channel(comm_file.m_path);
+
+ llvm::json::Object reverse_request = CreateRunInTerminalReverseRequest(
+ launch_request, g_vsc.debug_adaptor_path, comm_file.m_path);
llvm::json::Object reverse_response;
lldb_vscode::PacketStatus status =
g_vsc.SendReverseRequest(reverse_request, reverse_response);
if (status != lldb_vscode::PacketStatus::Success)
- error.SetErrorString("Process cannot be launched by IDE.");
+ return llvm::createStringError(llvm::inconvertibleErrorCode(),
+ "Process cannot be launched by the IDE. %s",
+ comm_channel.GetLauncherError().c_str());
- if (error.Success()) {
- // Wait for the attach stop event to happen or for a timeout.
- g_vsc.waiting_for_run_in_terminal = true;
- static std::mutex mutex;
- std::unique_lock<std::mutex> locker(mutex);
- g_vsc.request_in_terminal_cv.wait_for(locker, std::chrono::seconds(10));
+ if (llvm::Expected<lldb::pid_t> pid = comm_channel.GetLauncherPid())
+ attach_info.SetProcessID(*pid);
+ else
+ return pid.takeError();
- auto attached_pid = g_vsc.target.GetProcess().GetProcessID();
- if (attached_pid == LLDB_INVALID_PROCESS_ID)
- error.SetErrorString("Failed to attach to a process");
- else
- SendProcessEvent(Attach);
- }
+ g_vsc.debugger.SetAsync(false);
+ lldb::SBError error;
+ g_vsc.target.Attach(attach_info, error);
- if (error.Fail()) {
- launch_response["success"] = llvm::json::Value(false);
- EmplaceSafeString(launch_response, "message",
- std::string(error.GetCString()));
- } else {
- launch_response["success"] = llvm::json::Value(true);
- g_vsc.SendJSON(CreateEventObject("initialized"));
- }
+ if (error.Fail())
+ return llvm::createStringError(llvm::inconvertibleErrorCode(),
+ "Failed to attach to the target process. %s",
+ comm_channel.GetLauncherError().c_str());
+ // This will notify the runInTerminal launcher that we attached.
+ // We have to make this async, as the function won't return until the launcher
+ // resumes and reads the data.
+ std::future<llvm::Error> did_attach_message_success =
+ comm_channel.NotifyDidAttach();
+
+ // We just attached to the runInTerminal launcher, which was waiting to be
+ // attached. We now resume it, so it can receive the didAttach notification
+ // and then perform the exec. Upon continuing, the debugger will stop the
+ // process right in the middle of the exec. To the user, what we are doing is
+ // transparent, as they will only be able to see the process since the exec,
+ // completely unaware of the preparatory work.
+ g_vsc.target.GetProcess().Continue();
+
+ // Now that the actual target is just starting (i.e. exec was just invoked),
+ // we return the debugger to its async state.
+ g_vsc.debugger.SetAsync(true);
+
+ // If sending the notification failed, the launcher should be dead by now and
+ // the async didAttach notification should have an error message, so we
+ // return it. Otherwise, everything was a success.
+ did_attach_message_success.wait();
+ return did_attach_message_success.get();
}
// "LaunchRequest": {
return;
}
- if (GetBoolean(arguments, "runInTerminal", false)) {
- request_runInTerminal(request, response);
- g_vsc.SendJSON(llvm::json::Value(std::move(response)));
- return;
- }
-
// Instantiate a launch info instance for the target.
auto launch_info = g_vsc.target.GetLaunchInfo();
// Run any pre run LLDB commands the user specified in the launch.json
g_vsc.RunPreRunCommands();
- if (launchCommands.empty()) {
+
+ if (GetBoolean(arguments, "runInTerminal", false)) {
+ if (llvm::Error err = request_runInTerminal(request))
+ error.SetErrorString(llvm::toString(std::move(err)).c_str());
+ } else if (launchCommands.empty()) {
// Disable async events so the launch will be successful when we return from
// the launch call and the launch will happen synchronously
g_vsc.debugger.SetAsync(false);
}
g_vsc.SendJSON(llvm::json::Value(std::move(response)));
- SendProcessEvent(Launch);
+ if (g_vsc.is_attach)
+ SendProcessEvent(Attach); // this happens when doing runInTerminal
+ else
+ SendProcessEvent(Launch);
g_vsc.SendJSON(llvm::json::Value(CreateEventObject("initialized")));
- // Reenable async events and start the event thread to catch async events.
- // g_vsc.debugger.SetAsync(true);
}
// "NextRequest": {
llvm::outs() << examples;
}
-int main(int argc, char *argv[]) {
+// If --launch-target is provided, this instance of lldb-vscode becomes a
+// runInTerminal launcher. It will ultimately launch the program specified in
+// the --launch-target argument, which is the original program the user wanted
+// to debug. This is done in such a way that the actual debug adaptor can
+// place breakpoints at the beginning of the program.
+//
+// The launcher will communicate with the debug adaptor using a fifo file in the
+// directory specified in the --comm-file argument.
+//
+// Regarding the actual flow, this launcher will first notify the debug adaptor
+// of its pid. Then, the launcher will be in a pending state waiting to be
+// attached by the adaptor.
+//
+// Once attached and resumed, the launcher will exec and become the program
+// specified by --launch-target, which is the original target the
+// user wanted to run.
+//
+// In case of errors launching the target, a suitable error message will be
+// emitted to the debug adaptor.
+void LaunchRunInTerminalTarget(llvm::opt::Arg &target_arg,
+ llvm::StringRef comm_file, char *argv[]) {
+#if defined(WIN_32)
+ llvm::errs() << "runInTerminal is not supported on Windows\n";
+ exit(EXIT_FAILURE);
+#else
+ RunInTerminalLauncherCommChannel comm_channel(comm_file);
+ if (llvm::Error err = comm_channel.NotifyPid()) {
+ llvm::errs() << llvm::toString(std::move(err)) << "\n";
+ exit(EXIT_FAILURE);
+ }
- // Initialize LLDB first before we do anything.
- lldb::SBDebugger::Initialize();
+ // We will wait to be attached with a timeout. We don't wait indefinitely
+ // using a signal to prevent being paused forever.
+
+ // This env var should be used only for tests.
+ const char *timeout_env_var = getenv("LLDB_VSCODE_RIT_TIMEOUT_IN_MS");
+ int timeout_in_ms =
+ timeout_env_var != nullptr ? atoi(timeout_env_var) : 20000;
+ if (llvm::Error err = comm_channel.WaitUntilDebugAdaptorAttaches(
+ std::chrono::milliseconds(timeout_in_ms))) {
+ llvm::errs() << llvm::toString(std::move(err)) << "\n";
+ exit(EXIT_FAILURE);
+ }
- RegisterRequestCallbacks();
+ const char *target = target_arg.getValue();
+ execvp(target, argv);
- int portno = -1;
+ std::string error = std::strerror(errno);
+ comm_channel.NotifyError(error);
+ llvm::errs() << error << "\n";
+ exit(EXIT_FAILURE);
+#endif
+}
+
+int main(int argc, char *argv[]) {
+ llvm::SmallString<256> program_path(argv[0]);
+ llvm::sys::fs::make_absolute(program_path);
+ g_vsc.debug_adaptor_path = program_path.str().str();
LLDBVSCodeOptTable T;
unsigned MAI, MAC;
llvm::ArrayRef<const char *> ArgsArr = llvm::makeArrayRef(argv + 1, argc);
llvm::opt::InputArgList input_args = T.ParseArgs(ArgsArr, MAI, MAC);
+ if (llvm::opt::Arg *target_arg = input_args.getLastArg(OPT_launch_target)) {
+ if (llvm::opt::Arg *comm_file = input_args.getLastArg(OPT_comm_file)) {
+ int target_args_pos = argc;
+ for (int i = 0; i < argc; i++)
+ if (strcmp(argv[i], "--launch-target") == 0) {
+ target_args_pos = i + 1;
+ break;
+ }
+ LaunchRunInTerminalTarget(*target_arg, comm_file->getValue(),
+ argv + target_args_pos);
+ } else {
+ llvm::errs() << "\"--launch-target\" requires \"--comm-file\" to be "
+ "specified\n";
+ exit(EXIT_FAILURE);
+ }
+ }
+
+ // Initialize LLDB first before we do anything.
+ lldb::SBDebugger::Initialize();
+
+ RegisterRequestCallbacks();
+
+ int portno = -1;
+
if (input_args.hasArg(OPT_help)) {
printHelp(T, llvm::sys::path::filename(argv[0]));
return 0;