GlobalCompilationDatabase.cpp
Headers.cpp
JSONRPCDispatcher.cpp
+ JSONTransport.cpp
Logger.cpp
Protocol.cpp
ProtocolHandlers.cpp
reply(nullptr);
}
-void ClangdLSPServer::onExit(ExitParams &Params) { IsDone = true; }
+void ClangdLSPServer::onExit(ExitParams &Params) {
+ // No work to do.
+ // JSONRPCDispatcher shuts down the transport after this notification.
+}
void ClangdLSPServer::onDocumentDidOpen(DidOpenTextDocumentParams &Params) {
PathRef File = Params.textDocument.uri.file();
});
}
-ClangdLSPServer::ClangdLSPServer(JSONOutput &Out,
+ClangdLSPServer::ClangdLSPServer(class Transport &Transport,
const clangd::CodeCompleteOptions &CCOpts,
llvm::Optional<Path> CompileCommandsDir,
bool ShouldUseInMemoryCDB,
const ClangdServer::Options &Opts)
- : Out(Out), CDB(ShouldUseInMemoryCDB ? CompilationDB::makeInMemory()
- : CompilationDB::makeDirectoryBased(
- std::move(CompileCommandsDir))),
+ : Transport(Transport),
+ CDB(ShouldUseInMemoryCDB ? CompilationDB::makeInMemory()
+ : CompilationDB::makeDirectoryBased(
+ std::move(CompileCommandsDir))),
CCOpts(CCOpts), SupportedSymbolKinds(defaultSymbolKinds()),
SupportedCompletionItemKinds(defaultCompletionItemKinds()),
Server(new ClangdServer(CDB.getCDB(), FSProvider, /*DiagConsumer=*/*this,
Opts)) {}
-bool ClangdLSPServer::run(std::FILE *In, JSONStreamStyle InputStyle) {
- assert(!IsDone && "Run was called before");
+bool ClangdLSPServer::run() {
assert(Server);
// Set up JSONRPCDispatcher.
JSONRPCDispatcher Dispatcher([](const json::Value &Params) {
replyError(ErrorCode::MethodNotFound, "method not found");
+ return true;
});
registerCallbackHandlers(Dispatcher, /*Callbacks=*/*this);
// Run the Language Server loop.
- runLanguageServerLoop(In, Out, InputStyle, Dispatcher, IsDone);
+ bool CleanExit = true;
+ if (auto Err = Dispatcher.runLanguageServerLoop(Transport)) {
+ elog("Transport error: {0}", std::move(Err));
+ CleanExit = false;
+ }
- // Make sure IsDone is set to true after this method exits to ensure assertion
- // at the start of the method fires if it's ever executed again.
- IsDone = true;
// Destroy ClangdServer to ensure all worker threads finish.
Server.reset();
- return ShutdownRequestReceived;
+ return CleanExit && ShutdownRequestReceived;
}
std::vector<Fix> ClangdLSPServer::getFixes(StringRef File,
}
// Publish diagnostics.
- Out.writeMessage(json::Object{
- {"jsonrpc", "2.0"},
- {"method", "textDocument/publishDiagnostics"},
- {"params",
- json::Object{
- {"uri", URIForFile{File}},
- {"diagnostics", std::move(DiagnosticsJSON)},
- }},
- });
+ Transport.notify("textDocument/publishDiagnostics",
+ json::Object{
+ {"uri", URIForFile{File}},
+ {"diagnostics", std::move(DiagnosticsJSON)},
+ });
}
void ClangdLSPServer::reparseOpenedFiles() {
/// If \p CompileCommandsDir has a value, compile_commands.json will be
/// loaded only from \p CompileCommandsDir. Otherwise, clangd will look
/// for compile_commands.json in all parent directories of each file.
- ClangdLSPServer(JSONOutput &Out, const clangd::CodeCompleteOptions &CCOpts,
+ ClangdLSPServer(Transport &Transport,
+ const clangd::CodeCompleteOptions &CCOpts,
llvm::Optional<Path> CompileCommandsDir,
bool ShouldUseInMemoryCDB, const ClangdServer::Options &Opts);
- /// Run LSP server loop, receiving input for it from \p In. \p In must be
- /// opened in binary mode. Output will be written using Out variable passed to
- /// class constructor. This method must not be executed more than once for
- /// each instance of ClangdLSPServer.
+ /// Run LSP server loop, communicating with the Transport provided in the
+ /// constructor. This method must not be executed more than once.
///
- /// \return Whether we received a 'shutdown' request before an 'exit' request.
- bool run(std::FILE *In,
- JSONStreamStyle InputStyle = JSONStreamStyle::Standard);
+ /// \return Whether we shut down cleanly with a 'shutdown' -> 'exit' sequence.
+ bool run();
private:
// Implement DiagnosticsConsumer.
void reparseOpenedFiles();
void applyConfiguration(const ClangdConfigurationParamsChange &Settings);
- JSONOutput &Out;
/// Used to indicate that the 'shutdown' request was received from the
/// Language Server client.
bool ShutdownRequestReceived = false;
- /// Used to indicate that the 'exit' notification was received from the
- /// Language Server client.
- /// It's used to break out of the LSP parsing loop.
- bool IsDone = false;
-
std::mutex FixItsMutex;
typedef std::map<clangd::Diagnostic, std::vector<Fix>, LSPDiagnosticCompare>
DiagnosticToReplacementMap;
bool IsDirectoryBased;
};
+ clangd::Transport &Transport;
// Various ClangdServer parameters go here. It's important they're created
// before ClangdServer.
CompilationDB CDB;
#include "Cancellation.h"
#include "ProtocolHandlers.h"
#include "Trace.h"
+#include "Transport.h"
#include "llvm/ADT/ScopeExit.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/ADT/StringExtras.h"
namespace {
static Key<json::Value> RequestID;
-static Key<JSONOutput *> RequestOut;
+static Key<Transport *> CurrentTransport;
// When tracing, we trace a request and attach the response in reply().
// Because the Span isn't available, we find the current request using Context.
Key<std::unique_ptr<RequestSpan>> RequestSpan::RSKey;
} // namespace
-void JSONOutput::writeMessage(const json::Value &Message) {
- std::string S;
- llvm::raw_string_ostream OS(S);
- if (Pretty)
- OS << llvm::formatv("{0:2}", Message);
- else
- OS << Message;
- OS.flush();
-
- {
- std::lock_guard<std::mutex> Guard(StreamMutex);
- Outs << "Content-Length: " << S.size() << "\r\n\r\n" << S;
- Outs.flush();
- }
- vlog(">>> {0}\n", S);
-}
-
void JSONOutput::log(Logger::Level Level,
const llvm::formatv_object_base &Message) {
if (Level < MinLevel)
Logs.flush();
}
-void JSONOutput::mirrorInput(const Twine &Message) {
- if (!InputMirror)
- return;
-
- *InputMirror << Message;
- InputMirror->flush();
-}
-
void clangd::reply(json::Value &&Result) {
auto ID = getRequestId();
if (!ID) {
RequestSpan::attach([&](json::Object &Args) { Args["Reply"] = Result; });
log("--> reply({0})", *ID);
Context::current()
- .getExisting(RequestOut)
- ->writeMessage(json::Object{
- {"jsonrpc", "2.0"},
- {"id", *ID},
- {"result", std::move(Result)},
- });
+ .getExisting(CurrentTransport)
+ ->reply(std::move(*ID), std::move(Result));
}
void clangd::replyError(ErrorCode Code, const llvm::StringRef &Message) {
if (auto ID = getRequestId()) {
log("--> reply({0}) error: {1}", *ID, Message);
Context::current()
- .getExisting(RequestOut)
- ->writeMessage(json::Object{
- {"jsonrpc", "2.0"},
- {"id", *ID},
- {"error", json::Object{{"code", static_cast<int>(Code)},
- {"message", Message}}},
- });
+ .getExisting(CurrentTransport)
+ ->reply(std::move(*ID), make_error<LSPError>(Message, Code));
}
}
auto ID = 1;
log("--> {0}({1})", Method, ID);
Context::current()
- .getExisting(RequestOut)
- ->writeMessage(json::Object{
- {"jsonrpc", "2.0"},
- {"id", ID},
- {"method", Method},
- {"params", std::move(Params)},
- });
+ .getExisting(CurrentTransport)
+ ->call(Method, std::move(Params), ID);
}
JSONRPCDispatcher::JSONRPCDispatcher(Handler UnknownHandler)
: UnknownHandler(std::move(UnknownHandler)) {
registerHandler("$/cancelRequest", [this](const json::Value &Params) {
if (auto *O = Params.getAsObject())
- if (auto *ID = O->get("id"))
- return cancelRequest(*ID);
+ if (auto *ID = O->get("id")) {
+ cancelRequest(*ID);
+ return true;
+ }
log("Bad cancellation request: {0}", Params);
+ return true;
});
}
Handlers[Method] = std::move(H);
}
-static void logIncomingMessage(const llvm::Optional<json::Value> &ID,
- llvm::Optional<StringRef> Method,
- const json::Object *Error) {
- if (Method) { // incoming request
- if (ID) // call
- log("<-- {0}({1})", *Method, *ID);
- else // notification
- log("<-- {0}", *Method);
- } else if (ID) { // response, ID must be provided
- if (Error)
- log("<-- reply({0}) error: {1}", *ID,
- Error->getString("message").getValueOr("<no message>"));
- else
- log("<-- reply({0})", *ID);
- }
-}
-
-bool JSONRPCDispatcher::call(const json::Value &Message, JSONOutput &Out) {
- // Message must be an object with "jsonrpc":"2.0".
- auto *Object = Message.getAsObject();
- if (!Object || Object->getString("jsonrpc") != Optional<StringRef>("2.0"))
- return false;
- // ID may be any JSON value. If absent, this is a notification.
- llvm::Optional<json::Value> ID;
- if (auto *I = Object->get("id"))
- ID = std::move(*I);
- auto Method = Object->getString("method");
- logIncomingMessage(ID, Method, Object->getObject("error"));
- if (!Method) // We only handle incoming requests, and ignore responses.
- return false;
- // Params should be given, use null if not.
- json::Value Params = nullptr;
- if (auto *P = Object->get("params"))
- Params = std::move(*P);
-
- auto I = Handlers.find(*Method);
+bool JSONRPCDispatcher::onCall(StringRef Method, json::Value Params,
+ json::Value ID) {
+ log("<-- {0}({1})", Method, ID);
+ auto I = Handlers.find(Method);
auto &Handler = I != Handlers.end() ? I->second : UnknownHandler;
// Create a Context that contains request information.
- WithContextValue WithRequestOut(RequestOut, &Out);
- llvm::Optional<WithContextValue> WithID;
- if (ID)
- WithID.emplace(RequestID, *ID);
+ WithContextValue WithID(RequestID, ID);
// Create a tracing Span covering the whole request lifetime.
- trace::Span Tracer(*Method);
- if (ID)
- SPAN_ATTACH(Tracer, "ID", *ID);
+ trace::Span Tracer(Method);
+ SPAN_ATTACH(Tracer, "ID", ID);
SPAN_ATTACH(Tracer, "Params", Params);
- // Requests with IDs can be canceled by the client. Add cancellation context.
- llvm::Optional<WithContext> WithCancel;
- if (ID)
- WithCancel.emplace(cancelableRequestContext(*ID));
+ // Calls can be canceled by the client. Add cancellation context.
+ WithContext WithCancel(cancelableRequestContext(ID));
// Stash a reference to the span args, so later calls can add metadata.
WithContext WithRequestSpan(RequestSpan::stash(Tracer));
- Handler(std::move(Params));
+ return Handler(std::move(Params));
+}
+
+bool JSONRPCDispatcher::onNotify(StringRef Method, json::Value Params) {
+ log("<-- {0}", Method);
+ auto I = Handlers.find(Method);
+ auto &Handler = I != Handlers.end() ? I->second : UnknownHandler;
+
+ // Create a tracing Span covering the whole request lifetime.
+ trace::Span Tracer(Method);
+ SPAN_ATTACH(Tracer, "Params", Params);
+
+ // Stash a reference to the span args, so later calls can add metadata.
+ WithContext WithRequestSpan(RequestSpan::stash(Tracer));
+ return Handler(std::move(Params));
+}
+
+bool JSONRPCDispatcher::onReply(json::Value ID, Expected<json::Value> Result) {
+ // We ignore replies, just log them.
+ if (Result)
+ log("<-- reply({0})", ID);
+ else
+ log("<-- reply({0}) error: {1}", ID, llvm::toString(Result.takeError()));
return true;
}
It->second.first(); // Invoke the canceler.
}
-// Tries to read a line up to and including \n.
-// If failing, feof() or ferror() will be set.
-static bool readLine(std::FILE *In, std::string &Out) {
- static constexpr int BufSize = 1024;
- size_t Size = 0;
- Out.clear();
- for (;;) {
- Out.resize(Size + BufSize);
- // Handle EINTR which is sent when a debugger attaches on some platforms.
- if (!llvm::sys::RetryAfterSignal(nullptr, ::fgets, &Out[Size], BufSize, In))
- return false;
- clearerr(In);
- // If the line contained null bytes, anything after it (including \n) will
- // be ignored. Fortunately this is not a legal header or JSON.
- size_t Read = std::strlen(&Out[Size]);
- if (Read > 0 && Out[Size + Read - 1] == '\n') {
- Out.resize(Size + Read);
- return true;
- }
- Size += Read;
- }
-}
-
-// Returns None when:
-// - ferror() or feof() are set.
-// - Content-Length is missing or empty (protocol error)
-static llvm::Optional<std::string> readStandardMessage(std::FILE *In,
- JSONOutput &Out) {
- // A Language Server Protocol message starts with a set of HTTP headers,
- // delimited by \r\n, and terminated by an empty line (\r\n).
- unsigned long long ContentLength = 0;
- std::string Line;
- while (true) {
- if (feof(In) || ferror(In) || !readLine(In, Line))
- return llvm::None;
-
- Out.mirrorInput(Line);
- llvm::StringRef LineRef(Line);
-
- // We allow comments in headers. Technically this isn't part
- // of the LSP specification, but makes writing tests easier.
- if (LineRef.startswith("#"))
- continue;
-
- // Content-Length is a mandatory header, and the only one we handle.
- if (LineRef.consume_front("Content-Length: ")) {
- if (ContentLength != 0) {
- elog("Warning: Duplicate Content-Length header received. "
- "The previous value for this message ({0}) was ignored.",
- ContentLength);
- }
- llvm::getAsUnsignedInteger(LineRef.trim(), 0, ContentLength);
- continue;
- } else if (!LineRef.trim().empty()) {
- // It's another header, ignore it.
- continue;
- } else {
- // An empty line indicates the end of headers.
- // Go ahead and read the JSON.
- break;
- }
- }
-
- // The fuzzer likes crashing us by sending "Content-Length: 9999999999999999"
- if (ContentLength > 1 << 30) { // 1024M
- elog("Refusing to read message with long Content-Length: {0}. "
- "Expect protocol errors",
- ContentLength);
- return llvm::None;
- }
- if (ContentLength == 0) {
- log("Warning: Missing Content-Length header, or zero-length message.");
- return llvm::None;
- }
-
- std::string JSON(ContentLength, '\0');
- for (size_t Pos = 0, Read; Pos < ContentLength; Pos += Read) {
- // Handle EINTR which is sent when a debugger attaches on some platforms.
- Read = llvm::sys::RetryAfterSignal(0u, ::fread, &JSON[Pos], 1,
- ContentLength - Pos, In);
- Out.mirrorInput(StringRef(&JSON[Pos], Read));
- if (Read == 0) {
- elog("Input was aborted. Read only {0} bytes of expected {1}.", Pos,
- ContentLength);
- return llvm::None;
- }
- clearerr(In); // If we're done, the error was transient. If we're not done,
- // either it was transient or we'll see it again on retry.
- Pos += Read;
- }
- return std::move(JSON);
-}
-
-// For lit tests we support a simplified syntax:
-// - messages are delimited by '---' on a line by itself
-// - lines starting with # are ignored.
-// This is a testing path, so favor simplicity over performance here.
-// When returning None, feof() or ferror() will be set.
-static llvm::Optional<std::string> readDelimitedMessage(std::FILE *In,
- JSONOutput &Out) {
- std::string JSON;
- std::string Line;
- while (readLine(In, Line)) {
- auto LineRef = llvm::StringRef(Line).trim();
- if (LineRef.startswith("#")) // comment
- continue;
-
- // found a delimiter
- if (LineRef.rtrim() == "---")
- break;
-
- JSON += Line;
- }
-
- if (ferror(In)) {
- elog("Input error while reading message!");
- return llvm::None;
- } else { // Including EOF
- Out.mirrorInput(
- llvm::formatv("Content-Length: {0}\r\n\r\n{1}", JSON.size(), JSON));
- return std::move(JSON);
- }
-}
-
-// The use of C-style std::FILE* IO deserves some explanation.
-// Previously, std::istream was used. When a debugger attached on MacOS, the
-// process received EINTR, the stream went bad, and clangd exited.
-// A retry-on-EINTR loop around reads solved this problem, but caused clangd to
-// sometimes hang rather than exit on other OSes. The interaction between
-// istreams and signals isn't well-specified, so it's hard to get this right.
-// The C APIs seem to be clearer in this respect.
-void clangd::runLanguageServerLoop(std::FILE *In, JSONOutput &Out,
- JSONStreamStyle InputStyle,
- JSONRPCDispatcher &Dispatcher,
- bool &IsDone) {
- auto &ReadMessage =
- (InputStyle == Delimited) ? readDelimitedMessage : readStandardMessage;
- while (!IsDone && !feof(In)) {
- if (ferror(In)) {
- elog("IO error: {0}", llvm::sys::StrError());
- return;
- }
- if (auto JSON = ReadMessage(In, Out)) {
- if (auto Doc = json::parse(*JSON)) {
- // Log the formatted message.
- vlog(Out.Pretty ? "<<< {0:2}\n" : "<<< {0}\n", *Doc);
- // Finally, execute the action for this JSON message.
- if (!Dispatcher.call(*Doc, Out))
- elog("JSON dispatch failed!");
- } else {
- // Parse error. Log the raw message.
- vlog("<<< {0}\n", *JSON);
- elog("JSON parse error: {0}", llvm::toString(Doc.takeError()));
- }
- }
- }
+llvm::Error JSONRPCDispatcher::runLanguageServerLoop(Transport &Transport) {
+ // Propagate transport to all handlers so they can reply.
+ WithContextValue WithTransport(CurrentTransport, &Transport);
+ return Transport.loop(*this);
}
const json::Value *clangd::getRequestId() {
#include "Logger.h"
#include "Protocol.h"
#include "Trace.h"
+#include "Transport.h"
#include "clang/Basic/LLVM.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/ADT/StringMap.h"
namespace clang {
namespace clangd {
-/// Encapsulates output and logs streams and provides thread-safe access to
-/// them.
+// Logs to an output stream, such as stderr.
+// FIXME: Rename to StreamLogger or such, and move to Logger.h.
class JSONOutput : public Logger {
- // FIXME(ibiryukov): figure out if we can shrink the public interface of
- // JSONOutput now that we pass Context everywhere.
public:
- JSONOutput(llvm::raw_ostream &Outs, llvm::raw_ostream &Logs,
- Logger::Level MinLevel, llvm::raw_ostream *InputMirror = nullptr,
- bool Pretty = false)
- : Pretty(Pretty), MinLevel(MinLevel), Outs(Outs), Logs(Logs),
- InputMirror(InputMirror) {}
-
- /// Emit a JSONRPC message.
- void writeMessage(const llvm::json::Value &Result);
+ JSONOutput(llvm::raw_ostream &Logs, Logger::Level MinLevel)
+ : MinLevel(MinLevel), Logs(Logs) {}
/// Write a line to the logging stream.
void log(Level, const llvm::formatv_object_base &Message) override;
- /// Mirror \p Message into InputMirror stream. Does nothing if InputMirror is
- /// null.
- /// Unlike other methods of JSONOutput, mirrorInput is not thread-safe.
- void mirrorInput(const Twine &Message);
-
- // Whether output should be pretty-printed.
- const bool Pretty;
-
private:
Logger::Level MinLevel;
- llvm::raw_ostream &Outs;
llvm::raw_ostream &Logs;
- llvm::raw_ostream *InputMirror;
std::mutex StreamMutex;
};
///
/// The `$/cancelRequest` notification is handled by the dispatcher itself.
/// It marks the matching request as cancelled, if it's still running.
-class JSONRPCDispatcher {
+class JSONRPCDispatcher : private Transport::MessageHandler {
public:
/// A handler responds to requests for a particular method name.
+ /// It returns false if the server should now shut down.
///
/// JSONRPCDispatcher will mark the handler's context as cancelled if a
/// matching cancellation request is received. Handlers are encouraged to
/// check for cancellation and fail quickly in this case.
- using Handler = std::function<void(const llvm::json::Value &)>;
+ using Handler = std::function<bool(const llvm::json::Value &)>;
/// Create a new JSONRPCDispatcher. UnknownHandler is called when an unknown
/// method is received.
/// Registers a Handler for the specified Method.
void registerHandler(StringRef Method, Handler H);
- /// Parses a JSONRPC message and calls the Handler for it.
- bool call(const llvm::json::Value &Message, JSONOutput &Out);
+ /// Parses input queries from LSP client (coming from \p In) and runs call
+ /// method for each query.
+ ///
+ /// Input stream(\p In) must be opened in binary mode to avoid
+ /// preliminary replacements of \r\n with \n. We use C-style FILE* for reading
+ /// as std::istream has unclear interaction with signals, which are sent by
+ /// debuggers on some OSs.
+ llvm::Error runLanguageServerLoop(Transport &);
private:
+ bool onReply(llvm::json::Value ID,
+ llvm::Expected<llvm::json::Value> Result) override;
+ bool onNotify(llvm::StringRef Method, llvm::json::Value Message) override;
+ bool onCall(llvm::StringRef Method, llvm::json::Value Message,
+ llvm::json::Value ID) override;
+
// Tracking cancellations needs a mutex: handlers may finish on a different
// thread, and that's when we clean up entries in the map.
mutable std::mutex RequestCancelersMutex;
Handler UnknownHandler;
};
-/// Controls the way JSON-RPC messages are encoded (both input and output).
-enum JSONStreamStyle {
- /// Encoding per the LSP specification, with mandatory Content-Length header.
- Standard,
- /// Messages are delimited by a '---' line. Comment lines start with #.
- Delimited
-};
-
-/// Parses input queries from LSP client (coming from \p In) and runs call
-/// method of \p Dispatcher for each query.
-/// After handling each query checks if \p IsDone is set true and exits the loop
-/// if it is.
-/// Input stream(\p In) must be opened in binary mode to avoid preliminary
-/// replacements of \r\n with \n.
-/// We use C-style FILE* for reading as std::istream has unclear interaction
-/// with signals, which are sent by debuggers on some OSs.
-void runLanguageServerLoop(std::FILE *In, JSONOutput &Out,
- JSONStreamStyle InputStyle,
- JSONRPCDispatcher &Dispatcher, bool &IsDone);
} // namespace clangd
} // namespace clang
--- /dev/null
+//===--- JSONTransport.cpp - sending and receiving LSP messages over JSON -===//
+//
+// The LLVM Compiler Infrastructure
+//
+// This file is distributed under the University of Illinois Open Source
+// License. See LICENSE.TXT for details.
+//
+//===----------------------------------------------------------------------===//
+#include "Logger.h"
+#include "Protocol.h" // For LSPError
+#include "Transport.h"
+#include "llvm/Support/Errno.h"
+
+using namespace llvm;
+namespace clang {
+namespace clangd {
+namespace {
+
+json::Object encodeError(Error E) {
+ std::string Message;
+ ErrorCode Code = ErrorCode::UnknownErrorCode;
+ if (Error Unhandled =
+ handleErrors(std::move(E), [&](const LSPError &L) -> Error {
+ Message = L.Message;
+ Code = L.Code;
+ return Error::success();
+ }))
+ Message = llvm::toString(std::move(Unhandled));
+
+ return json::Object{
+ {"message", std::move(Message)},
+ {"code", int64_t(Code)},
+ };
+}
+
+Error decodeError(const json::Object &O) {
+ std::string Msg = O.getString("message").getValueOr("Unspecified error");
+ if (auto Code = O.getInteger("code"))
+ return make_error<LSPError>(std::move(Msg), ErrorCode(*Code));
+ return make_error<StringError>(std::move(Msg), inconvertibleErrorCode());
+}
+
+class JSONTransport : public Transport {
+public:
+ JSONTransport(std::FILE *In, llvm::raw_ostream &Out,
+ llvm::raw_ostream *InMirror, bool Pretty, JSONStreamStyle Style)
+ : In(In), Out(Out), InMirror(InMirror ? *InMirror : nulls()),
+ Pretty(Pretty), Style(Style) {}
+
+ void notify(StringRef Method, json::Value Params) override {
+ sendMessage(json::Object{
+ {"jsonrpc", "2.0"},
+ {"method", Method},
+ {"params", std::move(Params)},
+ });
+ }
+ void call(StringRef Method, json::Value Params, json::Value ID) override {
+ sendMessage(json::Object{
+ {"jsonrpc", "2.0"},
+ {"id", std::move(ID)},
+ {"method", Method},
+ {"params", std::move(Params)},
+ });
+ }
+ void reply(json::Value ID, Expected<json::Value> Result) override {
+ if (Result) {
+ sendMessage(json::Object{
+ {"jsonrpc", "2.0"},
+ {"id", std::move(ID)},
+ {"result", std::move(*Result)},
+ });
+ } else {
+ sendMessage(json::Object{
+ {"jsonrpc", "2.0"},
+ {"id", std::move(ID)},
+ {"error", encodeError(Result.takeError())},
+ });
+ }
+ }
+
+ Error loop(MessageHandler &Handler) override {
+ while (!feof(In)) {
+ if (ferror(In))
+ return errorCodeToError(std::error_code(errno, std::system_category()));
+ if (auto JSON = readRawMessage()) {
+ if (auto Doc = json::parse(*JSON)) {
+ vlog(Pretty ? "<<< {0:2}\n" : "<<< {0}\n", *Doc);
+ if (!handleMessage(std::move(*Doc), Handler))
+ return Error::success(); // we saw the "exit" notification.
+ } else {
+ // Parse error. Log the raw message.
+ vlog("<<< {0}\n", *JSON);
+ elog("JSON parse error: {0}", llvm::toString(Doc.takeError()));
+ }
+ }
+ }
+ return errorCodeToError(std::make_error_code(std::errc::io_error));
+ }
+
+private:
+ // Dispatches incoming message to Handler onNotify/onCall/onReply.
+ bool handleMessage(llvm::json::Value Message, MessageHandler &Handler);
+ // Writes outgoing message to Out stream.
+ void sendMessage(llvm::json::Value Message) {
+ std::string S;
+ llvm::raw_string_ostream OS(S);
+ OS << llvm::formatv(Pretty ? "{0:2}" : "{0}", Message);
+ OS.flush();
+ Out << "Content-Length: " << S.size() << "\r\n\r\n" << S;
+ Out.flush();
+ vlog(">>> {0}\n", S);
+ }
+
+ // Read raw string messages from input stream.
+ llvm::Optional<std::string> readRawMessage() {
+ return Style == JSONStreamStyle::Delimited ? readDelimitedMessage()
+ : readStandardMessage();
+ }
+ llvm::Optional<std::string> readDelimitedMessage();
+ llvm::Optional<std::string> readStandardMessage();
+
+ std::FILE *In;
+ llvm::raw_ostream &Out;
+ llvm::raw_ostream &InMirror;
+ bool Pretty;
+ JSONStreamStyle Style;
+};
+
+bool JSONTransport::handleMessage(llvm::json::Value Message,
+ MessageHandler &Handler) {
+ // Message must be an object with "jsonrpc":"2.0".
+ auto *Object = Message.getAsObject();
+ if (!Object || Object->getString("jsonrpc") != Optional<StringRef>("2.0")) {
+ elog("Not a JSON-RPC 2.0 message: {0:2}", Message);
+ return false;
+ }
+ // ID may be any JSON value. If absent, this is a notification.
+ llvm::Optional<json::Value> ID;
+ if (auto *I = Object->get("id"))
+ ID = std::move(*I);
+ auto Method = Object->getString("method");
+ if (!Method) { // This is a response.
+ if (!ID) {
+ elog("No method and no response ID: {0:2}", Message);
+ return false;
+ }
+ if (auto *Err = Object->getObject("error"))
+ return Handler.onReply(std::move(*ID), decodeError(*Err));
+ // Result should be given, use null if not.
+ json::Value Result = nullptr;
+ if (auto *R = Object->get("result"))
+ Result = std::move(*R);
+ return Handler.onReply(std::move(*ID), std::move(Result));
+ }
+ // Params should be given, use null if not.
+ json::Value Params = nullptr;
+ if (auto *P = Object->get("params"))
+ Params = std::move(*P);
+
+ if (ID)
+ return Handler.onCall(*Method, std::move(Params), std::move(*ID));
+ else
+ return Handler.onNotify(*Method, std::move(Params));
+}
+
+// Tries to read a line up to and including \n.
+// If failing, feof() or ferror() will be set.
+bool readLine(std::FILE *In, std::string &Out) {
+ static constexpr int BufSize = 1024;
+ size_t Size = 0;
+ Out.clear();
+ for (;;) {
+ Out.resize(Size + BufSize);
+ // Handle EINTR which is sent when a debugger attaches on some platforms.
+ if (!llvm::sys::RetryAfterSignal(nullptr, ::fgets, &Out[Size], BufSize, In))
+ return false;
+ clearerr(In);
+ // If the line contained null bytes, anything after it (including \n) will
+ // be ignored. Fortunately this is not a legal header or JSON.
+ size_t Read = std::strlen(&Out[Size]);
+ if (Read > 0 && Out[Size + Read - 1] == '\n') {
+ Out.resize(Size + Read);
+ return true;
+ }
+ Size += Read;
+ }
+}
+
+// Returns None when:
+// - ferror() or feof() are set.
+// - Content-Length is missing or empty (protocol error)
+llvm::Optional<std::string> JSONTransport::readStandardMessage() {
+ // A Language Server Protocol message starts with a set of HTTP headers,
+ // delimited by \r\n, and terminated by an empty line (\r\n).
+ unsigned long long ContentLength = 0;
+ std::string Line;
+ while (true) {
+ if (feof(In) || ferror(In) || !readLine(In, Line))
+ return llvm::None;
+ InMirror << Line;
+
+ llvm::StringRef LineRef(Line);
+
+ // We allow comments in headers. Technically this isn't part
+
+ // of the LSP specification, but makes writing tests easier.
+ if (LineRef.startswith("#"))
+ continue;
+
+ // Content-Length is a mandatory header, and the only one we handle.
+ if (LineRef.consume_front("Content-Length: ")) {
+ if (ContentLength != 0) {
+ elog("Warning: Duplicate Content-Length header received. "
+ "The previous value for this message ({0}) was ignored.",
+ ContentLength);
+ }
+ llvm::getAsUnsignedInteger(LineRef.trim(), 0, ContentLength);
+ continue;
+ } else if (!LineRef.trim().empty()) {
+ // It's another header, ignore it.
+ continue;
+ } else {
+ // An empty line indicates the end of headers.
+ // Go ahead and read the JSON.
+ break;
+ }
+ }
+
+ // The fuzzer likes crashing us by sending "Content-Length: 9999999999999999"
+ if (ContentLength > 1 << 30) { // 1024M
+ elog("Refusing to read message with long Content-Length: {0}. "
+ "Expect protocol errors",
+ ContentLength);
+ return llvm::None;
+ }
+ if (ContentLength == 0) {
+ log("Warning: Missing Content-Length header, or zero-length message.");
+ return llvm::None;
+ }
+
+ std::string JSON(ContentLength, '\0');
+ for (size_t Pos = 0, Read; Pos < ContentLength; Pos += Read) {
+ // Handle EINTR which is sent when a debugger attaches on some platforms.
+ Read = llvm::sys::RetryAfterSignal(0u, ::fread, &JSON[Pos], 1,
+ ContentLength - Pos, In);
+ if (Read == 0) {
+ elog("Input was aborted. Read only {0} bytes of expected {1}.", Pos,
+ ContentLength);
+ return llvm::None;
+ }
+ InMirror << StringRef(&JSON[Pos], Read);
+ clearerr(In); // If we're done, the error was transient. If we're not done,
+ // either it was transient or we'll see it again on retry.
+ Pos += Read;
+ }
+ return std::move(JSON);
+}
+
+// For lit tests we support a simplified syntax:
+// - messages are delimited by '---' on a line by itself
+// - lines starting with # are ignored.
+// This is a testing path, so favor simplicity over performance here.
+// When returning None, feof() or ferror() will be set.
+llvm::Optional<std::string> JSONTransport::readDelimitedMessage() {
+ std::string JSON;
+ std::string Line;
+ while (readLine(In, Line)) {
+ InMirror << Line;
+ auto LineRef = llvm::StringRef(Line).trim();
+ if (LineRef.startswith("#")) // comment
+ continue;
+
+ // found a delimiter
+ if (LineRef.rtrim() == "---")
+ break;
+
+ JSON += Line;
+ }
+
+ if (ferror(In)) {
+ elog("Input error while reading message!");
+ return llvm::None;
+ }
+ return std::move(JSON); // Including at EOF
+}
+
+} // namespace
+
+std::unique_ptr<Transport> newJSONTransport(std::FILE *In,
+ llvm::raw_ostream &Out,
+ llvm::raw_ostream *InMirror,
+ bool Pretty,
+ JSONStreamStyle Style) {
+ return llvm::make_unique<JSONTransport>(In, Out, InMirror, Pretty, Style);
+}
+
+} // namespace clangd
+} // namespace clang
namespace clangd {
using namespace llvm;
+char LSPError::ID;
+
URIForFile::URIForFile(std::string AbsPath) {
assert(llvm::sys::path::is_absolute(AbsPath) && "the path is relative");
File = std::move(AbsPath);
// Defined by the protocol.
RequestCancelled = -32800,
};
+// Models an LSP error as an llvm::Error.
+class LSPError : public llvm::ErrorInfo<LSPError> {
+public:
+ std::string Message;
+ ErrorCode Code;
+ static char ID;
+
+ LSPError(std::string Message, ErrorCode Code)
+ : Message(std::move(Message)), Code(Code) {}
+
+ void log(llvm::raw_ostream &OS) const override {
+ OS << int(Code) << ": " << Message;
+ }
+ std::error_code convertToErrorCode() const override {
+ return llvm::inconvertibleErrorCode();
+ }
+};
struct URIForFile {
URIForFile() = default;
} else {
elog("Failed to decode {0} request.", Method);
}
+ return Method != "exit"; // Shut down after exit notification.
});
}
--- /dev/null
+//===--- Transport.h - sending and receiving LSP messages -------*- C++ -*-===//
+//
+// The LLVM Compiler Infrastructure
+//
+// This file is distributed under the University of Illinois Open Source
+// License. See LICENSE.TXT for details.
+//
+//===----------------------------------------------------------------------===//
+//
+// The language server protocol is usually implemented by writing messages as
+// JSON-RPC over the stdin/stdout of a subprocess. However other communications
+// mechanisms are possible, such as XPC on mac.
+//
+// The Transport interface allows the mechanism to be replaced, and the JSONRPC
+// Transport is the standard implementation.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_TRANSPORT_H_
+#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_TRANSPORT_H_
+
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/JSON.h"
+#include "llvm/Support/raw_ostream.h"
+
+namespace clang {
+namespace clangd {
+
+// A transport is responsible for maintaining the connection to a client
+// application, and reading/writing structured messages to it.
+//
+// Transports have limited thread safety requirements:
+// - messages will not be sent concurrently
+// - messages MAY be sent while loop() is reading, or its callback is active
+class Transport {
+public:
+ virtual ~Transport() = default;
+
+ // Called by Clangd to send messages to the client.
+ virtual void notify(llvm::StringRef Method, llvm::json::Value Params) = 0;
+ virtual void call(llvm::StringRef Method, llvm::json::Value Params,
+ llvm::json::Value ID) = 0;
+ virtual void reply(llvm::json::Value ID,
+ llvm::Expected<llvm::json::Value> Result) = 0;
+
+ // Implemented by Clangd to handle incoming messages. (See loop() below).
+ class MessageHandler {
+ public:
+ virtual ~MessageHandler() = default;
+ // Handler returns true to keep processing messages, or false to shut down.
+ virtual bool onNotify(llvm::StringRef Method, llvm::json::Value) = 0;
+ virtual bool onCall(llvm::StringRef Method, llvm::json::Value Params,
+ llvm::json::Value ID) = 0;
+ virtual bool onReply(llvm::json::Value ID,
+ llvm::Expected<llvm::json::Value> Result) = 0;
+ };
+ // Called by Clangd to receive messages from the client.
+ // The transport should in turn invoke the handler to process messages.
+ // If handler returns false, the transport should immedately exit the loop.
+ // (This is used to implement the `exit` notification).
+ // Otherwise, it returns an error when the transport becomes unusable.
+ virtual llvm::Error loop(MessageHandler &) = 0;
+};
+
+// Controls the way JSON-RPC messages are encoded (both input and output).
+enum JSONStreamStyle {
+ // Encoding per the LSP specification, with mandatory Content-Length header.
+ Standard,
+ // Messages are delimited by a '---' line. Comment lines start with #.
+ Delimited
+};
+
+// Returns a Transport that speaks JSON-RPC over a pair of streams.
+// The input stream must be opened in binary mode.
+// If InMirror is set, data read will be echoed to it.
+//
+// The use of C-style std::FILE* input deserves some explanation.
+// Previously, std::istream was used. When a debugger attached on MacOS, the
+// process received EINTR, the stream went bad, and clangd exited.
+// A retry-on-EINTR loop around reads solved this problem, but caused clangd to
+// sometimes hang rather than exit on other OSes. The interaction between
+// istreams and signals isn't well-specified, so it's hard to get this right.
+// The C APIs seem to be clearer in this respect.
+std::unique_ptr<Transport>
+newJSONTransport(std::FILE *In, llvm::raw_ostream &Out,
+ llvm::raw_ostream *InMirror, bool Pretty,
+ JSONStreamStyle = JSONStreamStyle::Standard);
+
+} // namespace clangd
+} // namespace clang
+
+#endif
// Use buffered stream to stderr (we still flush each log message). Unbuffered
// stream can cause significant (non-deterministic) latency for the logger.
llvm::errs().SetBuffered();
- JSONOutput Out(llvm::outs(), llvm::errs(), LogLevel,
- InputMirrorStream ? InputMirrorStream.getPointer() : nullptr,
- PrettyPrint);
-
- clangd::LoggingSession LoggingSession(Out);
+ JSONOutput Logger(llvm::errs(), LogLevel);
+ clangd::LoggingSession LoggingSession(Logger);
// If --compile-commands-dir arg was invoked, check value and override default
// path.
CCOpts.AllScopes = AllScopesCompletion;
// Initialize and run ClangdLSPServer.
+ // Change stdin to binary to not lose \r\n on windows.
+ llvm::sys::ChangeStdinToBinary();
+ auto Transport = newJSONTransport(
+ stdin, llvm::outs(),
+ InputMirrorStream ? InputMirrorStream.getPointer() : nullptr, PrettyPrint,
+ InputStyle);
ClangdLSPServer LSPServer(
- Out, CCOpts, CompileCommandsDirPath,
+ *Transport, CCOpts, CompileCommandsDirPath,
/*ShouldUseInMemoryCDB=*/CompileArgsFrom == LSPCompileArgs, Opts);
constexpr int NoShutdownRequestErrorCode = 1;
llvm::set_thread_name("clangd.main");
- // Change stdin to binary to not lose \r\n on windows.
- llvm::sys::ChangeStdinToBinary();
- return LSPServer.run(stdin, InputStyle) ? 0 : NoShutdownRequestErrorCode;
+ return LSPServer.run() ? 0 : NoShutdownRequestErrorCode;
}
# CHECK-NEXT: "message": "MACRO is one",
---
{"jsonrpc":"2.0","id":10000,"method":"shutdown"}
+---
+{"jsonrpc":"2.0","method":"exit"}
# CHECK-NEXT: }
---
{"jsonrpc":"2.0","id":4,"method":"shutdown"}
+---
+{"jsonrpc":"2.0","method":"exit"}
# CHECK-NEXT: ]
---
{"jsonrpc":"2.0","id":4,"method":"shutdown"}
+---
+{"jsonrpc":"2.0","method":"exit"}
{"jsonrpc":"2.0","id":6,"method":"shutdown"}
---
{"jsonrpc":"2.0","method":"exit"}
+---
+{"jsonrpc":"2.0","method":"exit"}
{"jsonrpc":"2.0","id":9,"method":"workspace/executeCommand","params":{"arguments":[{"custom":"foo", "changes":{"test:///foo.c":[{"range":{"start":{"line":0,"character":32},"end":{"line":0,"character":32}},"newText":"("},{"range":{"start":{"line":0,"character":37},"end":{"line":0,"character":37}},"newText":")"}]}}],"command":"clangd.applyFix"}}
---
{"jsonrpc":"2.0","id":3,"method":"shutdown"}
+---
+{"jsonrpc":"2.0","method":"exit"}
Content-Length: 44\r
\r
{"jsonrpc":"2.0","id":3,"method":"shutdown"}\r
+Content-Length: 33\r
+\r
+{"jsonrpc":"2.0","method":"exit"}\r
# CHECK-NEXT: }
---
{"jsonrpc":"2.0","id":100000,"method":"shutdown"}
+---
+{"jsonrpc":"2.0","method":"exit"}
# CHECK-NEXT:}
---
{"jsonrpc":"2.0","id":4,"method":"shutdown"}
+---
+{"jsonrpc":"2.0","method":"exit"}
# CHECK: },
---
{"jsonrpc":"2.0","id":5,"method":"shutdown"}
+---
+{"jsonrpc":"2.0","method":"exit"}
# CHECK-NEXT: ]
---
{"jsonrpc":"2.0","id":10000,"method":"shutdown"}
+---
+{"jsonrpc":"2.0","method":"exit"}
GlobalCompilationDatabaseTests.cpp
HeadersTests.cpp
IndexTests.cpp
+ JSONTransportTests.cpp
QualityTests.cpp
RIFFTests.cpp
SerializationTests.cpp
--- /dev/null
+//===-- JSONTransportTests.cpp -------------------------------------------===//
+//
+// The LLVM Compiler Infrastructure
+//
+// This file is distributed under the University of Illinois Open Source
+// License. See LICENSE.TXT for details.
+//
+//===----------------------------------------------------------------------===//
+#include "Protocol.h"
+#include "Transport.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include <stdio.h>
+
+using namespace llvm;
+namespace clang {
+namespace clangd {
+namespace {
+
+// No fmemopen on windows, so we can't easily run this test.
+#ifndef WIN32
+
+// Fixture takes care of managing the input/output buffers for the transport.
+class JSONTransportTest : public ::testing::Test {
+ std::string InBuf, OutBuf, MirrorBuf;
+ llvm::raw_string_ostream Out, Mirror;
+ std::unique_ptr<FILE, int (*)(FILE *)> In;
+
+protected:
+ JSONTransportTest() : Out(OutBuf), Mirror(MirrorBuf), In(nullptr, nullptr) {}
+
+ template <typename... Args>
+ std::unique_ptr<Transport> transport(std::string InData, bool Pretty,
+ JSONStreamStyle Style) {
+ InBuf = std::move(InData);
+ In = {fmemopen(&InBuf[0], InBuf.size(), "r"), &fclose};
+ return newJSONTransport(In.get(), Out, &Mirror, Pretty, Style);
+ }
+
+ std::string input() const { return InBuf; }
+ std::string output() { return Out.str(); }
+ std::string input_mirror() { return Mirror.str(); }
+};
+
+// Echo is a simple server running on a transport:
+// - logs each message it gets.
+// - when it gets a call, replies to it
+// - when it gets a notification for method "call", makes a call on Target
+// Hangs up when it gets an exit notification.
+class Echo : public Transport::MessageHandler {
+ Transport &Target;
+ std::string LogBuf;
+ raw_string_ostream Log;
+
+public:
+ Echo(Transport &Target) : Target(Target), Log(LogBuf) {}
+
+ std::string log() { return Log.str(); }
+
+ bool onNotify(StringRef Method, json::Value Params) override {
+ Log << "Notification " << Method << ": " << Params << "\n";
+ if (Method == "call")
+ Target.call("echo call", std::move(Params), 42);
+ return Method != "exit";
+ }
+
+ bool onCall(StringRef Method, json::Value Params, json::Value ID) override {
+ Log << "Call " << Method << "(" << ID << "): " << Params << "\n";
+ if (Method == "err")
+ Target.reply(ID, make_error<LSPError>("trouble at mill", ErrorCode(88)));
+ else
+ Target.reply(ID, std::move(Params));
+ return true;
+ }
+
+ bool onReply(json::Value ID, Expected<json::Value> Params) override {
+ if (Params)
+ Log << "Reply(" << ID << "): " << *Params << "\n";
+ else
+ Log << "Reply(" << ID
+ << "): error = " << llvm::toString(Params.takeError()) << "\n";
+ return true;
+ }
+};
+
+std::string trim(StringRef S) { return S.trim().str(); }
+
+// Runs an Echo session using the standard JSON-RPC format we use in production.
+TEST_F(JSONTransportTest, StandardDense) {
+ auto T = transport(
+ "Content-Length: 52\r\n\r\n"
+ R"({"jsonrpc": "2.0", "method": "call", "params": 1234})"
+ "Content-Length: 46\r\n\r\n"
+ R"({"jsonrpc": "2.0", "id": 1234, "result": 5678})"
+ "Content-Length: 67\r\n\r\n"
+ R"({"jsonrpc": "2.0", "method": "foo", "id": "abcd", "params": "efgh"})"
+ "Content-Length: 73\r\n\r\n"
+ R"({"jsonrpc": "2.0", "id": "xyz", "error": {"code": 99, "message": "bad!"}})"
+ "Content-Length: 68\r\n\r\n"
+ R"({"jsonrpc": "2.0", "method": "err", "id": "wxyz", "params": "boom!"})"
+ "Content-Length: 36\r\n\r\n"
+ R"({"jsonrpc": "2.0", "method": "exit"})",
+ /*Pretty=*/false, JSONStreamStyle::Standard);
+ Echo E(*T);
+ auto Err = T->loop(E);
+ EXPECT_FALSE(bool(Err)) << llvm::toString(std::move(Err));
+
+ EXPECT_EQ(trim(E.log()), trim(R"(
+Notification call: 1234
+Reply(1234): 5678
+Call foo("abcd"): "efgh"
+Reply("xyz"): error = 99: bad!
+Call err("wxyz"): "boom!"
+Notification exit: null
+ )"));
+ EXPECT_EQ(
+ trim(output()),
+ "Content-Length: 60\r\n\r\n"
+ R"({"id":42,"jsonrpc":"2.0","method":"echo call","params":1234})"
+ "Content-Length: 45\r\n\r\n"
+ R"({"id":"abcd","jsonrpc":"2.0","result":"efgh"})"
+ "Content-Length: 77\r\n\r\n"
+ R"({"error":{"code":88,"message":"trouble at mill"},"id":"wxyz","jsonrpc":"2.0"})");
+ EXPECT_EQ(trim(input_mirror()), trim(input()));
+}
+
+// Runs an Echo session using the "delimited" input and pretty-printed output
+// that we use in lit tests.
+TEST_F(JSONTransportTest, DelimitedPretty) {
+ auto T = transport(R"jsonrpc(
+{"jsonrpc": "2.0", "method": "call", "params": 1234}
+---
+{"jsonrpc": "2.0", "id": 1234, "result": 5678}
+---
+{"jsonrpc": "2.0", "method": "foo", "id": "abcd", "params": "efgh"}
+---
+{"jsonrpc": "2.0", "id": "xyz", "error": {"code": 99, "message": "bad!"}}
+---
+{"jsonrpc": "2.0", "method": "err", "id": "wxyz", "params": "boom!"}
+---
+{"jsonrpc": "2.0", "method": "exit"}
+ )jsonrpc",
+ /*Pretty=*/true, JSONStreamStyle::Delimited);
+ Echo E(*T);
+ auto Err = T->loop(E);
+ EXPECT_FALSE(bool(Err)) << llvm::toString(std::move(Err));
+
+ EXPECT_EQ(trim(E.log()), trim(R"(
+Notification call: 1234
+Reply(1234): 5678
+Call foo("abcd"): "efgh"
+Reply("xyz"): error = 99: bad!
+Call err("wxyz"): "boom!"
+Notification exit: null
+ )"));
+ EXPECT_EQ(trim(output()), "Content-Length: 77\r\n\r\n"
+ R"({
+ "id": 42,
+ "jsonrpc": "2.0",
+ "method": "echo call",
+ "params": 1234
+})"
+ "Content-Length: 58\r\n\r\n"
+ R"({
+ "id": "abcd",
+ "jsonrpc": "2.0",
+ "result": "efgh"
+})"
+ "Content-Length: 105\r\n\r\n"
+ R"({
+ "error": {
+ "code": 88,
+ "message": "trouble at mill"
+ },
+ "id": "wxyz",
+ "jsonrpc": "2.0"
+})");
+ EXPECT_EQ(trim(input_mirror()), trim(input()));
+}
+
+// IO errors such as EOF ane reported.
+// The only successful return from loop() is if a handler returned false.
+TEST_F(JSONTransportTest, EndOfFile) {
+ auto T = transport("Content-Length: 52\r\n\r\n"
+ R"({"jsonrpc": "2.0", "method": "call", "params": 1234})",
+ /*Pretty=*/false, JSONStreamStyle::Standard);
+ Echo E(*T);
+ auto Err = T->loop(E);
+ EXPECT_EQ(trim(E.log()), "Notification call: 1234");
+ EXPECT_TRUE(bool(Err)); // Ran into EOF with no handler signalling done.
+ consumeError(std::move(Err));
+ EXPECT_EQ(trim(input_mirror()), trim(input()));
+}
+
+#endif
+
+} // namespace
+} // namespace clangd
+} // namespace clang