sammccall created this revision. sammccall added reviewers: jkorous, ioeric, hokein. Herald added subscribers: cfe-commits, kadircet, arphaman, MaskRay, ilya-biryukov, mgorny.
This paves the way for alternative transports (mac XPC, maybe messagepack?), and also generally improves layering: testing ClangdLSPServer becomes less of a pipe dream, we split up the JSONOutput monolith, etc. This isn't a final state, much of what remains in JSONRPCDispatcher can go away, handlers can call reply() on the transport directly, JSONOutput can be renamed to StreamLogger and removed, etc. But this patch is sprawling already. The main observable change (see tests) is that hitting EOF on input is now an error: the client should send the 'exit' notification. This is defensible: the protocol doesn't spell this case out. Reproducing the current behavior for all combinations of shutdown/exit/EOF clutters interfaces. We can iterate on this if desired. Repository: rCTE Clang Tools Extra https://reviews.llvm.org/D53286 Files: clangd/CMakeLists.txt clangd/ClangdLSPServer.cpp clangd/ClangdLSPServer.h clangd/JSONRPCDispatcher.cpp clangd/JSONRPCDispatcher.h clangd/JSONTransport.cpp clangd/Protocol.cpp clangd/Protocol.h clangd/ProtocolHandlers.cpp clangd/Transport.h clangd/tool/ClangdMain.cpp test/clangd/compile-commands-path-in-initialize.test test/clangd/compile-commands-path.test test/clangd/completion-snippets.test test/clangd/completion.test test/clangd/crash-non-added-files.test test/clangd/execute-command.test test/clangd/input-mirror.test test/clangd/signature-help.test test/clangd/textdocument-didchange-fail.test test/clangd/trace.test test/clangd/xrefs.test
Index: test/clangd/xrefs.test =================================================================== --- test/clangd/xrefs.test +++ test/clangd/xrefs.test @@ -55,3 +55,5 @@ # CHECK-NEXT: ] --- {"jsonrpc":"2.0","id":10000,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: test/clangd/trace.test =================================================================== --- test/clangd/trace.test +++ test/clangd/trace.test @@ -21,3 +21,5 @@ # CHECK: }, --- {"jsonrpc":"2.0","id":5,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: test/clangd/textdocument-didchange-fail.test =================================================================== --- test/clangd/textdocument-didchange-fail.test +++ 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: test/clangd/signature-help.test =================================================================== --- test/clangd/signature-help.test +++ test/clangd/signature-help.test @@ -23,3 +23,5 @@ # CHECK-NEXT: } --- {"jsonrpc":"2.0","id":100000,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: test/clangd/input-mirror.test =================================================================== --- test/clangd/input-mirror.test +++ 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: test/clangd/execute-command.test =================================================================== --- test/clangd/execute-command.test +++ 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: test/clangd/crash-non-added-files.test =================================================================== --- test/clangd/crash-non-added-files.test +++ 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: test/clangd/completion.test =================================================================== --- test/clangd/completion.test +++ test/clangd/completion.test @@ -68,3 +68,5 @@ # CHECK-NEXT: ] --- {"jsonrpc":"2.0","id":4,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: test/clangd/completion-snippets.test =================================================================== --- test/clangd/completion-snippets.test +++ test/clangd/completion-snippets.test @@ -52,3 +52,5 @@ # CHECK-NEXT: } --- {"jsonrpc":"2.0","id":4,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: test/clangd/compile-commands-path.test =================================================================== --- test/clangd/compile-commands-path.test +++ test/clangd/compile-commands-path.test @@ -40,3 +40,5 @@ # CHECK-NEXT: "message": "MACRO is two", --- {"jsonrpc":"2.0","id":10000,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: test/clangd/compile-commands-path-in-initialize.test =================================================================== --- test/clangd/compile-commands-path-in-initialize.test +++ test/clangd/compile-commands-path-in-initialize.test @@ -33,3 +33,5 @@ # CHECK-NEXT: "message": "MACRO is two", --- {"jsonrpc":"2.0","id":10000,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clangd/tool/ClangdMain.cpp =================================================================== --- clangd/tool/ClangdMain.cpp +++ 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. @@ -316,12 +313,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: clangd/Transport.h =================================================================== --- /dev/null +++ clangd/Transport.h @@ -0,0 +1,83 @@ +//===--- 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; + virtual bool notify(llvm::StringRef Method, llvm::json::Value) = 0; + virtual bool call(llvm::StringRef Method, llvm::json::Value Params, + llvm::json::Value ID) = 0; + virtual bool reply(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 true, the transport should immedately return success. + // (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. +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 Index: clangd/ProtocolHandlers.cpp =================================================================== --- clangd/ProtocolHandlers.cpp +++ clangd/ProtocolHandlers.cpp @@ -35,6 +35,7 @@ } else { elog("Failed to decode {0} request.", Method); } + return Method == "exit"; }); } Index: clangd/Protocol.h =================================================================== --- clangd/Protocol.h +++ 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<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; Index: clangd/Protocol.cpp =================================================================== --- clangd/Protocol.cpp +++ 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: clangd/JSONTransport.cpp =================================================================== --- /dev/null +++ clangd/JSONTransport.cpp @@ -0,0 +1,293 @@ +//===--- 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) { + ErrorCode Code = ErrorCode::UnknownErrorCode; + std::string Msg = toString(handleErrors(std::move(E), [&](const LSPError &L) { + Code = L.Code; + return make_error<StringError>(L.Message, inconvertibleErrorCode()); + })); + return json::Object{ + {"message", std::move(Msg)}, + {"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(); + } 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 notify/call/reply. + 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 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.reply(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.reply(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.call(*Method, std::move(Params), std::move(*ID)); + else + return Handler.notify(*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 Index: clangd/JSONRPCDispatcher.h =================================================================== --- clangd/JSONRPCDispatcher.h +++ 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. +// TODO: rename and move. 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,26 +64,39 @@ /// /// 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 true 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. JSONRPCDispatcher(Handler UnknownHandler); /// 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 reply(llvm::json::Value ID, + llvm::Expected<llvm::json::Value> Result) override; + bool notify(llvm::StringRef Method, llvm::json::Value Message) override; + bool call(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: clangd/JSONRPCDispatcher.cpp =================================================================== --- clangd/JSONRPCDispatcher.cpp +++ 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<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. @@ -58,23 +59,6 @@ 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) @@ -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<int>(Code)}, - {"message", Message}}}, - }); + .getExisting(CurrentTransport) + ->reply(std::move(*ID), make_error<LSPError>(Message, Code)); } } @@ -151,89 +118,71 @@ 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 false; + } log("Bad cancellation request: {0}", Params); + return false; }); } void JSONRPCDispatcher::registerHandler(StringRef Method, Handler H) { assert(!Handlers.count(Method) && "Handler already registered!"); 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::call(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)); + return Handler(std::move(Params)); +} + +bool JSONRPCDispatcher::notify(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 true; + return Handler(std::move(Params)); +} + +bool JSONRPCDispatcher::reply(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 false; } // We run cancelable requests in a context that does two things: @@ -266,162 +215,17 @@ 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() { Index: clangd/ClangdLSPServer.h =================================================================== --- clangd/ClangdLSPServer.h +++ clangd/ClangdLSPServer.h @@ -37,7 +37,8 @@ /// 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); @@ -47,8 +48,7 @@ /// each instance of ClangdLSPServer. /// /// \return Whether we received a 'shutdown' request before an 'exit' request. - bool run(std::FILE *In, - JSONStreamStyle InputStyle = JSONStreamStyle::Standard); + bool run(); private: // Implement DiagnosticsConsumer. @@ -89,16 +89,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<clangd::Diagnostic, std::vector<Fix>, LSPDiagnosticCompare> DiagnosticToReplacementMap; @@ -153,6 +147,7 @@ bool IsDirectoryBased; }; + clangd::Transport &Transport; // Various ClangdServer parameters go here. It's important they're created // before ClangdServer. CompilationDB CDB; Index: clangd/ClangdLSPServer.cpp =================================================================== --- clangd/ClangdLSPServer.cpp +++ clangd/ClangdLSPServer.cpp @@ -152,7 +152,9 @@ reply(nullptr); } -void ClangdLSPServer::onExit(ExitParams &Params) { IsDone = true; } +void ClangdLSPServer::onExit(ExitParams &Params) { + // XXX handler should return true. +} void ClangdLSPServer::onDocumentDidOpen(DidOpenTextDocumentParams &Params) { PathRef File = Params.textDocument.uri.file(); @@ -470,39 +472,41 @@ }); } -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 false; }); 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, @@ -562,15 +566,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: clangd/CMakeLists.txt =================================================================== --- clangd/CMakeLists.txt +++ clangd/CMakeLists.txt @@ -26,6 +26,7 @@ GlobalCompilationDatabase.cpp Headers.cpp JSONRPCDispatcher.cpp + JSONTransport.cpp Logger.cpp Protocol.cpp ProtocolHandlers.cpp
_______________________________________________ cfe-commits mailing list cfe-commits@lists.llvm.org http://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits