diff options
Diffstat (limited to 'clangd/JSONRPCDispatcher.cpp')
-rw-r--r-- | clangd/JSONRPCDispatcher.cpp | 236 |
1 files changed, 141 insertions, 95 deletions
diff --git a/clangd/JSONRPCDispatcher.cpp b/clangd/JSONRPCDispatcher.cpp index e86b09da..2741c665 100644 --- a/clangd/JSONRPCDispatcher.cpp +++ b/clangd/JSONRPCDispatcher.cpp @@ -8,28 +8,31 @@ //===----------------------------------------------------------------------===// #include "JSONRPCDispatcher.h" -#include "JSONExpr.h" #include "ProtocolHandlers.h" #include "Trace.h" #include "llvm/ADT/SmallString.h" #include "llvm/ADT/StringExtras.h" #include "llvm/Support/Chrono.h" +#include "llvm/Support/Errno.h" +#include "llvm/Support/FormatVariadic.h" +#include "llvm/Support/JSON.h" #include "llvm/Support/SourceMgr.h" #include <istream> +using namespace llvm; using namespace clang; using namespace clangd; namespace { -static Key<json::Expr> RequestID; +static Key<json::Value> RequestID; static Key<JSONOutput *> RequestOut; // When tracing, we trace a request and attach the repsonse in reply(). // Because the Span isn't available, we find the current request using Context. class RequestSpan { - RequestSpan(json::obj *Args) : Args(Args) {} + RequestSpan(llvm::json::Object *Args) : Args(Args) {} std::mutex Mu; - json::obj *Args; + llvm::json::Object *Args; static Key<std::unique_ptr<RequestSpan>> RSKey; public: @@ -40,7 +43,7 @@ public: } // If there's an enclosing request and the tracer is interested, calls \p F - // with a json::obj where request info can be added. + // with a json::Object where request info can be added. template <typename Func> static void attach(Func &&F) { auto *RequestArgs = Context::current().get(RSKey); if (!RequestArgs || !*RequestArgs || !(*RequestArgs)->Args) @@ -52,7 +55,7 @@ public: Key<std::unique_ptr<RequestSpan>> RequestSpan::RSKey; } // namespace -void JSONOutput::writeMessage(const json::Expr &Message) { +void JSONOutput::writeMessage(const json::Value &Message) { std::string S; llvm::raw_string_ostream OS(S); if (Pretty) @@ -66,14 +69,18 @@ void JSONOutput::writeMessage(const json::Expr &Message) { Outs << "Content-Length: " << S.size() << "\r\n\r\n" << S; Outs.flush(); } - log(llvm::Twine("--> ") + S); + vlog(">>> {0}\n", S); } -void JSONOutput::log(const Twine &Message) { +void JSONOutput::log(Logger::Level Level, + const llvm::formatv_object_base &Message) { + if (Level < MinLevel) + return; llvm::sys::TimePoint<> Timestamp = std::chrono::system_clock::now(); trace::log(Message); std::lock_guard<std::mutex> Guard(StreamMutex); - Logs << llvm::formatv("[{0:%H:%M:%S.%L}] {1}\n", Timestamp, Message); + Logs << llvm::formatv("{0}[{1:%H:%M:%S.%L}] {2}\n", indicator(Level), + Timestamp, Message); Logs.flush(); } @@ -85,52 +92,56 @@ void JSONOutput::mirrorInput(const Twine &Message) { InputMirror->flush(); } -void clangd::reply(json::Expr &&Result) { +void clangd::reply(json::Value &&Result) { auto ID = Context::current().get(RequestID); if (!ID) { - log("Attempted to reply to a notification!"); + elog("Attempted to reply to a notification!"); return; } - RequestSpan::attach([&](json::obj &Args) { Args["Reply"] = Result; }); + RequestSpan::attach([&](json::Object &Args) { Args["Reply"] = Result; }); + log("--> reply({0})", *ID); Context::current() .getExisting(RequestOut) - ->writeMessage(json::obj{ + ->writeMessage(json::Object{ {"jsonrpc", "2.0"}, {"id", *ID}, {"result", std::move(Result)}, }); } -void clangd::replyError(ErrorCode code, const llvm::StringRef &Message) { - log("Error " + Twine(static_cast<int>(code)) + ": " + Message); - RequestSpan::attach([&](json::obj &Args) { - Args["Error"] = - json::obj{{"code", static_cast<int>(code)}, {"message", Message.str()}}; +void clangd::replyError(ErrorCode Code, const llvm::StringRef &Message) { + elog("Error {0}: {1}", static_cast<int>(Code), Message); + RequestSpan::attach([&](json::Object &Args) { + Args["Error"] = json::Object{{"code", static_cast<int>(Code)}, + {"message", Message.str()}}; }); if (auto ID = Context::current().get(RequestID)) { + log("--> reply({0}) error: {1}", *ID, Message); Context::current() .getExisting(RequestOut) - ->writeMessage(json::obj{ + ->writeMessage(json::Object{ {"jsonrpc", "2.0"}, {"id", *ID}, - {"error", - json::obj{{"code", static_cast<int>(code)}, {"message", Message}}}, + {"error", json::Object{{"code", static_cast<int>(Code)}, + {"message", Message}}}, }); } } -void clangd::call(StringRef Method, json::Expr &&Params) { +void clangd::call(StringRef Method, json::Value &&Params) { + RequestSpan::attach([&](json::Object &Args) { + Args["Call"] = json::Object{{"method", Method.str()}, {"params", Params}}; + }); // FIXME: Generate/Increment IDs for every request so that we can get proper // replies once we need to. - RequestSpan::attach([&](json::obj &Args) { - Args["Call"] = json::obj{{"method", Method.str()}, {"params", Params}}; - }); + auto ID = 1; + log("--> {0}({1})", Method, ID); Context::current() .getExisting(RequestOut) - ->writeMessage(json::obj{ + ->writeMessage(json::Object{ {"jsonrpc", "2.0"}, - {"id", 1}, + {"id", ID}, {"method", Method}, {"params", std::move(Params)}, }); @@ -141,21 +152,39 @@ void JSONRPCDispatcher::registerHandler(StringRef Method, Handler H) { Handlers[Method] = std::move(H); } -bool JSONRPCDispatcher::call(const json::Expr &Message, JSONOutput &Out) const { +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) const { // Message must be an object with "jsonrpc":"2.0". - auto *Object = Message.asObject(); + 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::Expr> ID; + llvm::Optional<json::Value> ID; if (auto *I = Object->get("id")) ID = std::move(*I); - // Method must be given. auto Method = Object->getString("method"); - if (!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::Expr Params = nullptr; + json::Value Params = nullptr; if (auto *P = Object->get("params")) Params = std::move(*P); @@ -180,27 +209,43 @@ bool JSONRPCDispatcher::call(const json::Expr &Message, JSONOutput &Out) const { return true; } -static llvm::Optional<std::string> readStandardMessage(std::istream &In, +// 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; - while (In.good()) { - std::string Line; - std::getline(In, Line); - if (!In.good() && errno == EINTR) { - In.clear(); - continue; - } + std::string Line; + while (true) { + if (feof(In) || ferror(In) || !readLine(In, Line)) + return llvm::None; Out.mirrorInput(Line); - // Mirror '\n' that gets consumed by std::getline, but is not included in - // the resulting Line. - // Note that '\r' is part of Line, so we don't need to mirror it - // separately. - if (!In.eof()) - Out.mirrorInput("\n"); - llvm::StringRef LineRef(Line); // We allow comments in headers. Technically this isn't part @@ -208,19 +253,13 @@ static llvm::Optional<std::string> readStandardMessage(std::istream &In, if (LineRef.startswith("#")) continue; - // Content-Type is a specified header, but does nothing. - // Content-Length is a mandatory header. It specifies the length of the - // following JSON. - // It is unspecified what sequence headers must be supplied in, so we - // allow any sequence. - // The end of headers is signified by an empty line. + // Content-Length is a mandatory header, and the only one we handle. if (LineRef.consume_front("Content-Length: ")) { if (ContentLength != 0) { - log("Warning: Duplicate Content-Length header received. " - "The previous value for this message (" + - llvm::Twine(ContentLength) + ") was ignored.\n"); + 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()) { @@ -233,46 +272,46 @@ static llvm::Optional<std::string> readStandardMessage(std::istream &In, } } - // Guard against large messages. This is usually a bug in the client code - // and we don't want to crash downstream because of it. + // The fuzzer likes crashing us by sending "Content-Length: 9999999999999999" if (ContentLength > 1 << 30) { // 1024M - In.ignore(ContentLength); - log("Skipped overly large message of " + Twine(ContentLength) + - " bytes.\n"); + 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; } - if (ContentLength > 0) { - std::string JSON(ContentLength, '\0'); - In.read(&JSON[0], ContentLength); - Out.mirrorInput(StringRef(JSON.data(), In.gcount())); - - // If the stream is aborted before we read ContentLength bytes, In - // will have eofbit and failbit set. - if (!In) { - log("Input was aborted. Read only " + llvm::Twine(In.gcount()) + - " bytes of expected " + llvm::Twine(ContentLength) + ".\n"); + 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; } - return std::move(JSON); - } else { - log("Warning: Missing Content-Length header, or message has zero " - "length.\n"); - 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. -static llvm::Optional<std::string> readDelimitedMessage(std::istream &In, +// 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 (std::getline(In, Line)) { - Line.push_back('\n'); // getline() consumed the newline. - + while (readLine(In, Line)) { auto LineRef = llvm::StringRef(Line).trim(); if (LineRef.startswith("#")) // comment continue; @@ -284,39 +323,46 @@ static llvm::Optional<std::string> readDelimitedMessage(std::istream &In, JSON += Line; } - if (In.bad()) { - log("Input error while reading message!"); + if (ferror(In)) { + elog("Input error while reading message!"); return llvm::None; - } else { + } else { // Including EOF Out.mirrorInput( llvm::formatv("Content-Length: {0}\r\n\r\n{1}", JSON.size(), JSON)); return std::move(JSON); } } -void clangd::runLanguageServerLoop(std::istream &In, JSONOutput &Out, +// 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 (In.good()) { + 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. - log(llvm::formatv(Out.Pretty ? "<-- {0:2}\n" : "<-- {0}\n", *Doc)); + vlog(Out.Pretty ? "<<< {0:2}\n" : "<<< {0}\n", *Doc); // Finally, execute the action for this JSON message. if (!Dispatcher.call(*Doc, Out)) - log("JSON dispatch failed!\n"); + elog("JSON dispatch failed!"); } else { // Parse error. Log the raw message. - log(llvm::formatv("<-- {0}\n" , *JSON)); - log(llvm::Twine("JSON parse error: ") + - llvm::toString(Doc.takeError()) + "\n"); + vlog("<<< {0}\n", *JSON); + elog("JSON parse error: {0}", llvm::toString(Doc.takeError())); } } - // If we're done, exit the loop. - if (IsDone) - break; } } |