Skip to content

Commit de51b2d

Browse files
authored
[lldb] Move Transport class into lldb_private (NFC) (#143806)
Move lldb-dap's Transport class into lldb_private so the code can be shared between the "JSON with header" protocol used by DAP and the JSON RPC protocol used by MCP (see [1]). [1]: https://discourse.llvm.org/t/rfc-adding-mcp-support-to-lldb/86798
1 parent bb3b830 commit de51b2d

File tree

9 files changed

+308
-211
lines changed

9 files changed

+308
-211
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
//===-- JSONTransport.h ---------------------------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
//
9+
// Transport layer for encoding and decoding JSON protocol messages.
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
#ifndef LLDB_HOST_JSONTRANSPORT_H
14+
#define LLDB_HOST_JSONTRANSPORT_H
15+
16+
#include "lldb/lldb-forward.h"
17+
#include "llvm/ADT/StringRef.h"
18+
#include "llvm/Support/Error.h"
19+
#include "llvm/Support/FormatVariadic.h"
20+
#include "llvm/Support/JSON.h"
21+
#include <chrono>
22+
#include <system_error>
23+
24+
namespace lldb_private {
25+
26+
class TransportEOFError : public llvm::ErrorInfo<TransportEOFError> {
27+
public:
28+
static char ID;
29+
30+
TransportEOFError() = default;
31+
32+
void log(llvm::raw_ostream &OS) const override {
33+
OS << "transport end of file reached";
34+
}
35+
std::error_code convertToErrorCode() const override {
36+
return llvm::inconvertibleErrorCode();
37+
}
38+
};
39+
40+
class TransportTimeoutError : public llvm::ErrorInfo<TransportTimeoutError> {
41+
public:
42+
static char ID;
43+
44+
TransportTimeoutError() = default;
45+
46+
void log(llvm::raw_ostream &OS) const override {
47+
OS << "transport operation timed out";
48+
}
49+
std::error_code convertToErrorCode() const override {
50+
return std::make_error_code(std::errc::timed_out);
51+
}
52+
};
53+
54+
class TransportClosedError : public llvm::ErrorInfo<TransportClosedError> {
55+
public:
56+
static char ID;
57+
58+
TransportClosedError() = default;
59+
60+
void log(llvm::raw_ostream &OS) const override {
61+
OS << "transport is closed";
62+
}
63+
std::error_code convertToErrorCode() const override {
64+
return llvm::inconvertibleErrorCode();
65+
}
66+
};
67+
68+
/// A transport class that uses JSON for communication.
69+
class JSONTransport {
70+
public:
71+
JSONTransport(lldb::IOObjectSP input, lldb::IOObjectSP output);
72+
virtual ~JSONTransport() = default;
73+
74+
/// Transport is not copyable.
75+
/// @{
76+
JSONTransport(const JSONTransport &rhs) = delete;
77+
void operator=(const JSONTransport &rhs) = delete;
78+
/// @}
79+
80+
/// Writes a message to the output stream.
81+
template <typename T> llvm::Error Write(const T &t) {
82+
const std::string message = llvm::formatv("{0}", toJSON(t)).str();
83+
return WriteImpl(message);
84+
}
85+
86+
/// Reads the next message from the input stream.
87+
template <typename T>
88+
llvm::Expected<T> Read(const std::chrono::microseconds &timeout) {
89+
llvm::Expected<std::string> message = ReadImpl(timeout);
90+
if (!message)
91+
return message.takeError();
92+
return llvm::json::parse<T>(/*JSON=*/*message);
93+
}
94+
95+
protected:
96+
virtual void Log(llvm::StringRef message);
97+
98+
virtual llvm::Error WriteImpl(const std::string &message) = 0;
99+
virtual llvm::Expected<std::string>
100+
ReadImpl(const std::chrono::microseconds &timeout) = 0;
101+
102+
lldb::IOObjectSP m_input;
103+
lldb::IOObjectSP m_output;
104+
};
105+
106+
/// A transport class for JSON with a HTTP header.
107+
class HTTPDelimitedJSONTransport : public JSONTransport {
108+
public:
109+
HTTPDelimitedJSONTransport(lldb::IOObjectSP input, lldb::IOObjectSP output)
110+
: JSONTransport(input, output) {}
111+
virtual ~HTTPDelimitedJSONTransport() = default;
112+
113+
protected:
114+
virtual llvm::Error WriteImpl(const std::string &message) override;
115+
virtual llvm::Expected<std::string>
116+
ReadImpl(const std::chrono::microseconds &timeout) override;
117+
118+
// FIXME: Support any header.
119+
static constexpr llvm::StringLiteral kHeaderContentLength =
120+
"Content-Length: ";
121+
static constexpr llvm::StringLiteral kHeaderSeparator = "\r\n\r\n";
122+
};
123+
124+
} // namespace lldb_private
125+
126+
#endif

lldb/source/Host/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ add_host_subdirectory(common
2727
common/HostNativeThreadBase.cpp
2828
common/HostProcess.cpp
2929
common/HostThread.cpp
30-
common/LockFileBase.cpp
30+
common/JSONTransport.cpp
3131
common/LZMA.cpp
32+
common/LockFileBase.cpp
3233
common/MainLoopBase.cpp
3334
common/MemoryMonitor.cpp
3435
common/MonitoringProcessLauncher.cpp
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
//===-- JSONTransport.cpp -------------------------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#include "lldb/Host/JSONTransport.h"
10+
#include "lldb/Utility/IOObject.h"
11+
#include "lldb/Utility/LLDBLog.h"
12+
#include "lldb/Utility/Log.h"
13+
#include "lldb/Utility/SelectHelper.h"
14+
#include "lldb/Utility/Status.h"
15+
#include "lldb/lldb-forward.h"
16+
#include "llvm/ADT/StringExtras.h"
17+
#include "llvm/ADT/StringRef.h"
18+
#include "llvm/Support/Error.h"
19+
#include "llvm/Support/raw_ostream.h"
20+
#include <optional>
21+
#include <string>
22+
#include <utility>
23+
24+
using namespace llvm;
25+
using namespace lldb;
26+
using namespace lldb_private;
27+
28+
/// ReadFull attempts to read the specified number of bytes. If EOF is
29+
/// encountered, an empty string is returned.
30+
static Expected<std::string>
31+
ReadFull(IOObject &descriptor, size_t length,
32+
std::optional<std::chrono::microseconds> timeout = std::nullopt) {
33+
if (!descriptor.IsValid())
34+
return llvm::make_error<TransportClosedError>();
35+
36+
bool timeout_supported = true;
37+
// FIXME: SelectHelper does not work with NativeFile on Win32.
38+
#if _WIN32
39+
timeout_supported = descriptor.GetFdType() == IOObject::eFDTypeSocket;
40+
#endif
41+
42+
if (timeout && timeout_supported) {
43+
SelectHelper sh;
44+
sh.SetTimeout(*timeout);
45+
sh.FDSetRead(descriptor.GetWaitableHandle());
46+
Status status = sh.Select();
47+
if (status.Fail()) {
48+
// Convert timeouts into a specific error.
49+
if (status.GetType() == lldb::eErrorTypePOSIX &&
50+
status.GetError() == ETIMEDOUT)
51+
return make_error<TransportTimeoutError>();
52+
return status.takeError();
53+
}
54+
}
55+
56+
std::string data;
57+
data.resize(length);
58+
Status status = descriptor.Read(data.data(), length);
59+
if (status.Fail())
60+
return status.takeError();
61+
62+
// Read returns '' on EOF.
63+
if (length == 0)
64+
return make_error<TransportEOFError>();
65+
66+
// Return the actual number of bytes read.
67+
return data.substr(0, length);
68+
}
69+
70+
static Expected<std::string>
71+
ReadUntil(IOObject &descriptor, StringRef delimiter,
72+
std::optional<std::chrono::microseconds> timeout = std::nullopt) {
73+
std::string buffer;
74+
buffer.reserve(delimiter.size() + 1);
75+
while (!llvm::StringRef(buffer).ends_with(delimiter)) {
76+
Expected<std::string> next =
77+
ReadFull(descriptor, buffer.empty() ? delimiter.size() : 1, timeout);
78+
if (auto Err = next.takeError())
79+
return std::move(Err);
80+
buffer += *next;
81+
}
82+
return buffer.substr(0, buffer.size() - delimiter.size());
83+
}
84+
85+
JSONTransport::JSONTransport(IOObjectSP input, IOObjectSP output)
86+
: m_input(std::move(input)), m_output(std::move(output)) {}
87+
88+
void JSONTransport::Log(llvm::StringRef message) {
89+
LLDB_LOG(GetLog(LLDBLog::Host), "{0}", message);
90+
}
91+
92+
Expected<std::string>
93+
HTTPDelimitedJSONTransport::ReadImpl(const std::chrono::microseconds &timeout) {
94+
if (!m_input || !m_input->IsValid())
95+
return createStringError("transport output is closed");
96+
97+
IOObject *input = m_input.get();
98+
Expected<std::string> message_header =
99+
ReadFull(*input, kHeaderContentLength.size(), timeout);
100+
if (!message_header)
101+
return message_header.takeError();
102+
if (*message_header != kHeaderContentLength)
103+
return createStringError(formatv("expected '{0}' and got '{1}'",
104+
kHeaderContentLength, *message_header)
105+
.str());
106+
107+
Expected<std::string> raw_length = ReadUntil(*input, kHeaderSeparator);
108+
if (!raw_length)
109+
return handleErrors(raw_length.takeError(),
110+
[&](const TransportEOFError &E) -> llvm::Error {
111+
return createStringError(
112+
"unexpected EOF while reading header separator");
113+
});
114+
115+
size_t length;
116+
if (!to_integer(*raw_length, length))
117+
return createStringError(
118+
formatv("invalid content length {0}", *raw_length).str());
119+
120+
Expected<std::string> raw_json = ReadFull(*input, length);
121+
if (!raw_json)
122+
return handleErrors(
123+
raw_json.takeError(), [&](const TransportEOFError &E) -> llvm::Error {
124+
return createStringError("unexpected EOF while reading JSON");
125+
});
126+
127+
Log(llvm::formatv("--> {0}", *raw_json).str());
128+
129+
return raw_json;
130+
}
131+
132+
Error HTTPDelimitedJSONTransport::WriteImpl(const std::string &message) {
133+
if (!m_output || !m_output->IsValid())
134+
return llvm::make_error<TransportClosedError>();
135+
136+
Log(llvm::formatv("<-- {0}", message).str());
137+
138+
std::string Output;
139+
raw_string_ostream OS(Output);
140+
OS << kHeaderContentLength << message.length() << kHeaderSeparator << message;
141+
size_t num_bytes = Output.size();
142+
return m_output->Write(Output.data(), num_bytes).takeError();
143+
}
144+
145+
char TransportEOFError::ID;
146+
char TransportTimeoutError::ID;
147+
char TransportClosedError::ID;

lldb/tools/lldb-dap/DAP.cpp

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070

7171
using namespace lldb_dap;
7272
using namespace lldb_dap::protocol;
73+
using namespace lldb_private;
7374

7475
namespace {
7576
#ifdef _WIN32
@@ -893,14 +894,14 @@ llvm::Error DAP::Loop() {
893894

894895
while (!disconnecting) {
895896
llvm::Expected<Message> next =
896-
transport.Read(std::chrono::seconds(1));
897-
if (next.errorIsA<EndOfFileError>()) {
897+
transport.Read<protocol::Message>(std::chrono::seconds(1));
898+
if (next.errorIsA<TransportEOFError>()) {
898899
consumeError(next.takeError());
899900
break;
900901
}
901902

902903
// If the read timed out, continue to check if we should disconnect.
903-
if (next.errorIsA<TimeoutError>()) {
904+
if (next.errorIsA<TransportTimeoutError>()) {
904905
consumeError(next.takeError());
905906
continue;
906907
}

0 commit comments

Comments
 (0)