Index: clang-tools-extra/trunk/clangd/CMakeLists.txt =================================================================== --- clang-tools-extra/trunk/clangd/CMakeLists.txt +++ clang-tools-extra/trunk/clangd/CMakeLists.txt @@ -26,6 +26,7 @@ GlobalCompilationDatabase.cpp Headers.cpp JSONRPCDispatcher.cpp + JSONTransport.cpp Logger.cpp Protocol.cpp ProtocolHandlers.cpp Index: clang-tools-extra/trunk/clangd/ClangdLSPServer.h =================================================================== --- clang-tools-extra/trunk/clangd/ClangdLSPServer.h +++ clang-tools-extra/trunk/clangd/ClangdLSPServer.h @@ -37,18 +37,16 @@ /// 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 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. @@ -89,16 +87,10 @@ 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, LSPDiagnosticCompare> DiagnosticToReplacementMap; @@ -153,6 +145,7 @@ bool IsDirectoryBased; }; + clangd::Transport &Transport; // Various ClangdServer parameters go here. It's important they're created // before ClangdServer. CompilationDB CDB; Index: clang-tools-extra/trunk/clangd/ClangdLSPServer.cpp =================================================================== --- clang-tools-extra/trunk/clangd/ClangdLSPServer.cpp +++ clang-tools-extra/trunk/clangd/ClangdLSPServer.cpp @@ -162,7 +162,10 @@ 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(); @@ -497,39 +500,41 @@ }); } -ClangdLSPServer::ClangdLSPServer(JSONOutput &Out, +ClangdLSPServer::ClangdLSPServer(class Transport &Transport, const clangd::CodeCompleteOptions &CCOpts, llvm::Optional 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 ClangdLSPServer::getFixes(StringRef File, @@ -589,15 +594,11 @@ } // 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() { Index: clang-tools-extra/trunk/clangd/JSONRPCDispatcher.h =================================================================== --- clang-tools-extra/trunk/clangd/JSONRPCDispatcher.h +++ clang-tools-extra/trunk/clangd/JSONRPCDispatcher.h @@ -14,6 +14,7 @@ #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" @@ -24,37 +25,19 @@ 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; }; @@ -81,14 +64,15 @@ /// /// 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; + using Handler = std::function; /// Create a new JSONRPCDispatcher. UnknownHandler is called when an unknown /// method is received. @@ -97,10 +81,22 @@ /// 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 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; @@ -113,25 +109,6 @@ 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 Index: clang-tools-extra/trunk/clangd/JSONRPCDispatcher.cpp =================================================================== --- clang-tools-extra/trunk/clangd/JSONRPCDispatcher.cpp +++ clang-tools-extra/trunk/clangd/JSONRPCDispatcher.cpp @@ -11,6 +11,7 @@ #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" @@ -28,7 +29,7 @@ namespace { static Key RequestID; -static Key RequestOut; +static Key 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. @@ -58,23 +59,6 @@ Key> 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 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) @@ -87,14 +71,6 @@ 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) { @@ -104,12 +80,8 @@ 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) { @@ -122,13 +94,8 @@ 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(Code)}, - {"message", Message}}}, - }); + .getExisting(CurrentTransport) + ->reply(std::move(*ID), make_error(Message, Code)); } } @@ -151,22 +118,20 @@ 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; }); } @@ -175,64 +140,48 @@ Handlers[Method] = std::move(H); } -static void logIncomingMessage(const llvm::Optional &ID, - llvm::Optional 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("")); - 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("2.0")) - return false; - // ID may be any JSON value. If absent, this is a notification. - llvm::Optional 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 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 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)); + 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)); - Handler(std::move(Params)); + return Handler(std::move(Params)); +} + +bool JSONRPCDispatcher::onReply(json::Value ID, Expected 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; } @@ -266,162 +215,10 @@ 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 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 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() { Index: clang-tools-extra/trunk/clangd/JSONTransport.cpp =================================================================== --- clang-tools-extra/trunk/clangd/JSONTransport.cpp +++ clang-tools-extra/trunk/clangd/JSONTransport.cpp @@ -0,0 +1,298 @@ +//===--- 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(std::move(Msg), ErrorCode(*Code)); + return make_error(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 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 readRawMessage() { + return Style == JSONStreamStyle::Delimited ? readDelimitedMessage() + : readStandardMessage(); + } + llvm::Optional readDelimitedMessage(); + llvm::Optional 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("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 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 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 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 newJSONTransport(std::FILE *In, + llvm::raw_ostream &Out, + llvm::raw_ostream *InMirror, + bool Pretty, + JSONStreamStyle Style) { + return llvm::make_unique(In, Out, InMirror, Pretty, Style); +} + +} // namespace clangd +} // namespace clang Index: clang-tools-extra/trunk/clangd/Protocol.h =================================================================== --- clang-tools-extra/trunk/clangd/Protocol.h +++ clang-tools-extra/trunk/clangd/Protocol.h @@ -48,6 +48,23 @@ // Defined by the protocol. RequestCancelled = -32800, }; +// Models an LSP error as an llvm::Error. +class LSPError : public llvm::ErrorInfo { +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; Index: clang-tools-extra/trunk/clangd/Protocol.cpp =================================================================== --- clang-tools-extra/trunk/clangd/Protocol.cpp +++ clang-tools-extra/trunk/clangd/Protocol.cpp @@ -25,6 +25,8 @@ 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); Index: clang-tools-extra/trunk/clangd/ProtocolHandlers.cpp =================================================================== --- clang-tools-extra/trunk/clangd/ProtocolHandlers.cpp +++ clang-tools-extra/trunk/clangd/ProtocolHandlers.cpp @@ -35,6 +35,7 @@ } else { elog("Failed to decode {0} request.", Method); } + return Method != "exit"; // Shut down after exit notification. }); } Index: clang-tools-extra/trunk/clangd/Transport.h =================================================================== --- clang-tools-extra/trunk/clangd/Transport.h +++ clang-tools-extra/trunk/clangd/Transport.h @@ -0,0 +1,92 @@ +//===--- 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 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 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 +newJSONTransport(std::FILE *In, llvm::raw_ostream &Out, + llvm::raw_ostream *InMirror, bool Pretty, + JSONStreamStyle = JSONStreamStyle::Standard); + +} // namespace clangd +} // namespace clang + +#endif Index: clang-tools-extra/trunk/clangd/tool/ClangdMain.cpp =================================================================== --- clang-tools-extra/trunk/clangd/tool/ClangdMain.cpp +++ clang-tools-extra/trunk/clangd/tool/ClangdMain.cpp @@ -253,11 +253,8 @@ // 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. @@ -317,12 +314,16 @@ 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; } Index: clang-tools-extra/trunk/test/clangd/compile-commands-path-in-initialize.test =================================================================== --- clang-tools-extra/trunk/test/clangd/compile-commands-path-in-initialize.test +++ clang-tools-extra/trunk/test/clangd/compile-commands-path-in-initialize.test @@ -24,3 +24,5 @@ # CHECK-NEXT: "message": "MACRO is one", --- {"jsonrpc":"2.0","id":10000,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/test/clangd/completion-snippets.test =================================================================== --- clang-tools-extra/trunk/test/clangd/completion-snippets.test +++ clang-tools-extra/trunk/test/clangd/completion-snippets.test @@ -52,3 +52,5 @@ # CHECK-NEXT: } --- {"jsonrpc":"2.0","id":4,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/test/clangd/completion.test =================================================================== --- clang-tools-extra/trunk/test/clangd/completion.test +++ clang-tools-extra/trunk/test/clangd/completion.test @@ -68,3 +68,5 @@ # CHECK-NEXT: ] --- {"jsonrpc":"2.0","id":4,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/test/clangd/crash-non-added-files.test =================================================================== --- clang-tools-extra/trunk/test/clangd/crash-non-added-files.test +++ clang-tools-extra/trunk/test/clangd/crash-non-added-files.test @@ -32,3 +32,5 @@ {"jsonrpc":"2.0","id":6,"method":"shutdown"} --- {"jsonrpc":"2.0","method":"exit"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/test/clangd/execute-command.test =================================================================== --- clang-tools-extra/trunk/test/clangd/execute-command.test +++ clang-tools-extra/trunk/test/clangd/execute-command.test @@ -62,3 +62,5 @@ {"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"} Index: clang-tools-extra/trunk/test/clangd/input-mirror.test =================================================================== --- clang-tools-extra/trunk/test/clangd/input-mirror.test +++ clang-tools-extra/trunk/test/clangd/input-mirror.test @@ -12,3 +12,6 @@ Content-Length: 44 {"jsonrpc":"2.0","id":3,"method":"shutdown"} +Content-Length: 33 + +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/test/clangd/signature-help.test =================================================================== --- clang-tools-extra/trunk/test/clangd/signature-help.test +++ clang-tools-extra/trunk/test/clangd/signature-help.test @@ -23,3 +23,5 @@ # CHECK-NEXT: } --- {"jsonrpc":"2.0","id":100000,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/test/clangd/textdocument-didchange-fail.test =================================================================== --- clang-tools-extra/trunk/test/clangd/textdocument-didchange-fail.test +++ clang-tools-extra/trunk/test/clangd/textdocument-didchange-fail.test @@ -35,3 +35,5 @@ # CHECK-NEXT:} --- {"jsonrpc":"2.0","id":4,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/test/clangd/trace.test =================================================================== --- clang-tools-extra/trunk/test/clangd/trace.test +++ clang-tools-extra/trunk/test/clangd/trace.test @@ -21,3 +21,5 @@ # CHECK: }, --- {"jsonrpc":"2.0","id":5,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/test/clangd/xrefs.test =================================================================== --- clang-tools-extra/trunk/test/clangd/xrefs.test +++ clang-tools-extra/trunk/test/clangd/xrefs.test @@ -55,3 +55,5 @@ # CHECK-NEXT: ] --- {"jsonrpc":"2.0","id":10000,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/unittests/clangd/CMakeLists.txt =================================================================== --- clang-tools-extra/trunk/unittests/clangd/CMakeLists.txt +++ clang-tools-extra/trunk/unittests/clangd/CMakeLists.txt @@ -27,6 +27,7 @@ GlobalCompilationDatabaseTests.cpp HeadersTests.cpp IndexTests.cpp + JSONTransportTests.cpp QualityTests.cpp RIFFTests.cpp SerializationTests.cpp Index: clang-tools-extra/trunk/unittests/clangd/JSONTransportTests.cpp =================================================================== --- clang-tools-extra/trunk/unittests/clangd/JSONTransportTests.cpp +++ clang-tools-extra/trunk/unittests/clangd/JSONTransportTests.cpp @@ -0,0 +1,199 @@ +//===-- 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 + +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 In; + +protected: + JSONTransportTest() : Out(OutBuf), Mirror(MirrorBuf), In(nullptr, nullptr) {} + + template + std::unique_ptr 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("trouble at mill", ErrorCode(88))); + else + Target.reply(ID, std::move(Params)); + return true; + } + + bool onReply(json::Value ID, Expected 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