diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/lldbvscode_testcase.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/lldbvscode_testcase.py --- a/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/lldbvscode_testcase.py +++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/lldbvscode_testcase.py @@ -282,7 +282,8 @@ 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 ''' @@ -317,7 +318,12 @@ 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'])) @@ -325,6 +331,7 @@ # 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, @@ -340,7 +347,7 @@ 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) diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/vscode.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/vscode.py --- a/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/vscode.py +++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/vscode.py @@ -612,7 +612,7 @@ stopCommands=None, exitCommands=None, terminateCommands=None ,sourcePath=None, debuggerRoot=None, launchCommands=None, sourceMap=None, - runInTerminal=False): + runInTerminal=False, expectFailure=False): args_dict = { 'program': program } @@ -660,9 +660,10 @@ } 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): diff --git a/lldb/test/API/tools/lldb-vscode/runInTerminal/TestVSCode_runInTerminal.py b/lldb/test/API/tools/lldb-vscode/runInTerminal/TestVSCode_runInTerminal.py --- a/lldb/test/API/tools/lldb-vscode/runInTerminal/TestVSCode_runInTerminal.py +++ b/lldb/test/API/tools/lldb-vscode/runInTerminal/TestVSCode_runInTerminal.py @@ -11,22 +11,47 @@ 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]) @@ -46,3 +71,87 @@ # 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) diff --git a/lldb/tools/lldb-vscode/CMakeLists.txt b/lldb/tools/lldb-vscode/CMakeLists.txt --- a/lldb/tools/lldb-vscode/CMakeLists.txt +++ b/lldb/tools/lldb-vscode/CMakeLists.txt @@ -27,10 +27,12 @@ lldb-vscode.cpp BreakpointBase.cpp ExceptionBreakpoint.cpp + FifoFiles.cpp FunctionBreakpoint.cpp IOStream.cpp JSONUtils.cpp LLDBUtils.cpp + RunInTerminal.cpp SourceBreakpoint.cpp VSCode.cpp diff --git a/lldb/tools/lldb-vscode/FifoFiles.h b/lldb/tools/lldb-vscode/FifoFiles.h new file mode 100644 --- /dev/null +++ b/lldb/tools/lldb-vscode/FifoFiles.h @@ -0,0 +1,84 @@ +//===-- 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 if the file could be created, or an +/// \a llvm::Error in case of failures. +llvm::Expected> 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 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 diff --git a/lldb/tools/lldb-vscode/FifoFiles.cpp b/lldb/tools/lldb-vscode/FifoFiles.cpp new file mode 100644 --- /dev/null +++ b/lldb/tools/lldb-vscode/FifoFiles.cpp @@ -0,0 +1,91 @@ +//===-- 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 +#include +#include +#endif + +#include +#include +#include +#include + +#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> 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(path); +#endif +} + +FifoFileIO::FifoFileIO(StringRef fifo_file, StringRef other_endpoint_name) + : m_fifo_file(fifo_file), m_other_endpoint_name(other_endpoint_name) {} + +Expected 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 line; + std::future *future = + new std::future(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 *future = + new std::future(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 diff --git a/lldb/tools/lldb-vscode/JSONUtils.h b/lldb/tools/lldb-vscode/JSONUtils.h --- a/lldb/tools/lldb-vscode/JSONUtils.h +++ b/lldb/tools/lldb-vscode/JSONUtils.h @@ -449,11 +449,23 @@ /// 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 diff --git a/lldb/tools/lldb-vscode/JSONUtils.cpp b/lldb/tools/lldb-vscode/JSONUtils.cpp --- a/lldb/tools/lldb-vscode/JSONUtils.cpp +++ b/lldb/tools/lldb-vscode/JSONUtils.cpp @@ -1001,7 +1001,9 @@ /// 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"); @@ -1012,10 +1014,13 @@ run_in_terminal_args.try_emplace("kind", "integrated"); auto launch_request_arguments = launch_request.getObject("arguments"); - std::vector 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 args = { + debug_adaptor_path.str(), "--comm-file", comm_file.str(), + "--launch-target", GetString(launch_request_arguments, "program").str()}; + std::vector 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"); @@ -1038,4 +1043,12 @@ 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 diff --git a/lldb/tools/lldb-vscode/Options.td b/lldb/tools/lldb-vscode/Options.td --- a/lldb/tools/lldb-vscode/Options.td +++ b/lldb/tools/lldb-vscode/Options.td @@ -23,3 +23,14 @@ def: Separate<["-"], "p">, Alias, HelpText<"Alias for --port">; + +def launch_target: Separate<["--", "-"], "launch-target">, + MetaVarName<"">, + 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<"">, + HelpText<"The fifo file used to communicate the with the debug adaptor" + "when using --launch-target.">; diff --git a/lldb/tools/lldb-vscode/RunInTerminal.h b/lldb/tools/lldb-vscode/RunInTerminal.h new file mode 100644 --- /dev/null +++ b/lldb/tools/lldb-vscode/RunInTerminal.h @@ -0,0 +1,129 @@ +//===-- 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 +#include + +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; + +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 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 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> CreateRunInTerminalCommFile(); + +} // namespace lldb_vscode + +#endif // LLDB_TOOLS_LLDB_VSCODE_RUNINTERMINAL_H diff --git a/lldb/tools/lldb-vscode/RunInTerminal.cpp b/lldb/tools/lldb-vscode/RunInTerminal.cpp new file mode 100644 --- /dev/null +++ b/lldb/tools/lldb-vscode/RunInTerminal.cpp @@ -0,0 +1,167 @@ +//===-- 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 +#include +#include +#endif + +#include +#include +#include +#include + +#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(this); +} + +const RunInTerminalMessageError * +RunInTerminalMessage::GetAsErrorMessage() const { + return static_cast(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(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 +ParseJSONMessage(const json::Value &json) { + if (const json::Object *obj = json.getAsObject()) { + if (Optional kind = obj->getString("kind")) { + if (*kind == "pid") { + if (Optional pid = obj->getInteger("pid")) + return std::make_unique( + static_cast(*pid)); + } else if (*kind == "error") { + if (Optional error = obj->getString("error")) + return std::make_unique(*error); + } else if (*kind == "didAttach") { + return std::make_unique(); + } + } + } + + return createStringError(inconvertibleErrorCode(), + "Incorrect JSON message: " + JSONToString(json)); +} + +static Expected +GetNextMessage(FifoFileIO &io, std::chrono::milliseconds timeout) { + if (Expected 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 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 RunInTerminalDebugAdapterCommChannel::NotifyDidAttach() { + return std::async(std::launch::async, [&]() { + return m_io.SendJSON(RunInTerminalMessageDidAttach().ToJSON()); + }); +} + +Expected RunInTerminalDebugAdapterCommChannel::GetLauncherPid() { + if (Expected 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 message = + GetNextMessage(m_io, std::chrono::seconds(1))) + return toString(ToError(*message.get())); + else + return toString(message.takeError()); +} + +Expected> 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 diff --git a/lldb/tools/lldb-vscode/VSCode.h b/lldb/tools/lldb-vscode/VSCode.h --- a/lldb/tools/lldb-vscode/VSCode.h +++ b/lldb/tools/lldb-vscode/VSCode.h @@ -47,6 +47,7 @@ #include "ExceptionBreakpoint.h" #include "FunctionBreakpoint.h" #include "IOStream.h" +#include "RunInTerminal.h" #include "SourceBreakpoint.h" #include "SourceReference.h" @@ -77,6 +78,7 @@ }; struct VSCode { + std::string debug_adaptor_path; InputStream input; OutputStream output; lldb::SBDebugger debugger; @@ -104,7 +106,6 @@ bool is_attach; uint32_t reverse_request_seq; std::map 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. diff --git a/lldb/tools/lldb-vscode/lldb-vscode.cpp b/lldb/tools/lldb-vscode/lldb-vscode.cpp --- a/lldb/tools/lldb-vscode/lldb-vscode.cpp +++ b/lldb/tools/lldb-vscode/lldb-vscode.cpp @@ -384,12 +384,7 @@ 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); @@ -1457,47 +1452,64 @@ 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> 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 locker(mutex); - g_vsc.request_in_terminal_cv.wait_for(locker, std::chrono::seconds(10)); + if (llvm::Expected 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 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": { @@ -1572,12 +1584,6 @@ 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(); @@ -1613,7 +1619,11 @@ // 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); @@ -1632,10 +1642,11 @@ } 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": { @@ -2966,20 +2977,94 @@ 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 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;