Index: CMakeLists.txt =================================================================== --- CMakeLists.txt +++ CMakeLists.txt @@ -16,7 +16,8 @@ string(REPLACE ${CMAKE_CFG_INTDIR} ${LLVM_BUILD_MODE} CLANG_TOOLS_DIR ${LLVM_RUNTIME_OUTPUT_INTDIR}) llvm_canonicalize_cmake_booleans( - CLANG_ENABLE_STATIC_ANALYZER) + CLANG_ENABLE_STATIC_ANALYZER + CLANGD_BUILD_XPC) configure_lit_site_cfg( ${CMAKE_CURRENT_SOURCE_DIR}/lit.site.cfg.in @@ -72,6 +73,10 @@ ) endif() +if(CLANGD_BUILD_XPC) + list(APPEND CLANG_TOOLS_TEST_DEPS clangd-xpc-test-client) +endif() + set(llvm_utils FileCheck count not ) Index: Features.inc.in =================================================================== --- /dev/null +++ Features.inc.in @@ -0,0 +1 @@ +#define CLANGD_BUILD_XPC @CLANGD_BUILD_XPC@ Index: clangd/CMakeLists.txt =================================================================== --- clangd/CMakeLists.txt +++ clangd/CMakeLists.txt @@ -51,4 +51,4 @@ clangToolingInclusions LLVMSupport LLVMTestingSupport - ) + ) \ No newline at end of file Index: clangd/xpc/initialize.test =================================================================== --- /dev/null +++ clangd/xpc/initialize.test @@ -0,0 +1,10 @@ +# RUN: clangd-xpc-test-client < %s | FileCheck %s +# REQUIRES: clangd-xpc-support + +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootUri":"test:///workspace","capabilities":{},"trace":"off"}} +# CHECK-DAG: {"id":0,"jsonrpc":"2.0","result":{"capabilities":{"codeActionProvider":true,"completionProvider":{"resolveProvider":false,"triggerCharacters":[".",">",":"]},"definitionProvider":true,"documentFormattingProvider":true,"documentHighlightProvider":true,"documentOnTypeFormattingProvider":{"firstTriggerCharacter":"}","moreTriggerCharacter":[]},"documentRangeFormattingProvider":true,"documentSymbolProvider":true,"executeCommandProvider":{"commands":["clangd.applyFix"]},"hoverProvider":true,"renameProvider":true,"signatureHelpProvider":{"triggerCharacters":["(",","]},"textDocumentSync":2,"workspaceSymbolProvider":true}}} + +{"jsonrpc":"2.0","id":3,"method":"shutdown"} +# CHECK-DAG: {"id":3,"jsonrpc":"2.0","result":null} + +{"jsonrpc":"2.0","method":"exit"} Index: lit.cfg =================================================================== --- lit.cfg +++ lit.cfg @@ -117,6 +117,10 @@ if platform.system() not in ['Windows']: config.available_features.add('ansi-escape-sequences') +# XPC support for Clangd. +if config.clangd_xpc_support: + config.available_features.add('clangd-xpc-support') + if config.clang_staticanalyzer: config.available_features.add('static-analyzer') check_clang_tidy = os.path.join( Index: lit.site.cfg.in =================================================================== --- lit.site.cfg.in +++ lit.site.cfg.in @@ -11,6 +11,7 @@ config.python_executable = "@PYTHON_EXECUTABLE@" config.target_triple = "@TARGET_TRIPLE@" config.clang_staticanalyzer = @CLANG_ENABLE_STATIC_ANALYZER@ +config.clangd_xpc_support = @CLANGD_BUILD_XPC@ # Support substitution of the tools and libs dirs with user parameters. This is # used when we can't determine the tool dir at configuration time. Index: xpc/CMakeLists.txt =================================================================== --- /dev/null +++ xpc/CMakeLists.txt @@ -0,0 +1,17 @@ +set(CLANGD_XPC_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") +set(CLANGD_XPC_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}") + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules") +include(CreateClangdXPCFramework) + +add_subdirectory(framework) +add_subdirectory(test-client) +add_subdirectory(tool) + +include_directories( + ${CMAKE_CURRENT_SOURCE_DIR}/../ +) + +set(LLVM_LINK_COMPONENTS + Support + ) \ No newline at end of file Index: xpc/README.txt =================================================================== --- /dev/null +++ xpc/README.txt @@ -0,0 +1,6 @@ +This directory contains: +- the XPC transport layer (alternative transport layer to JSON-RPC) +- XPC framework wrapper that wraps around Clangd to make it a valid XPC service +- XPC test-client + +MacOS only. Feature is guarded by CLANGD_BUILD_XPC, including whole xpc/ dir. \ No newline at end of file Index: xpc/cmake/Info.plist.in =================================================================== --- /dev/null +++ xpc/cmake/Info.plist.in @@ -0,0 +1,28 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${CLANGD_XPC_FRAMEWORK_NAME} + CFBundleIconFile + + CFBundleIdentifier + org.llvm.${CLANGD_XPC_FRAMEWORK_NAME} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${CLANGD_XPC_FRAMEWORK_NAME} + CFBundlePackageType + FMWK + CFBundleSignature + ???? + CFBundleVersion + + CFBundleShortVersionString + 1.0 + CSResourcesFileMapped + + + Index: xpc/cmake/XPCServiceInfo.plist.in =================================================================== --- /dev/null +++ xpc/cmake/XPCServiceInfo.plist.in @@ -0,0 +1,30 @@ + + + + + CFBundleExecutable + clangd-xpc-adapter + CFBundleIdentifier + ${CLANGD_XPC_SERVICE_BUNDLE_NAME} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${CLANGD_XPC_SERVICE_NAME} + CFBundlePackageType + XPC! + CFBundleVersion + + CFBundleShortVersionString + 1.0 + XPCService + + ServiceType + Application + EnvironmentVariables + + CLANGD_PATH + ${CLANGD_PATH_IN_CLANGDXPC_SERVICE} + + + + Index: xpc/cmake/modules/CreateClangdXPCFramework.cmake =================================================================== --- /dev/null +++ xpc/cmake/modules/CreateClangdXPCFramework.cmake @@ -0,0 +1,81 @@ +# Creates the ClangdXPC framework. +macro(create_clangd_xpc_framework target name) + set(CLANGD_FRAMEWORK_LOCATION "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${name}.framework") + set(CLANGD_FRAMEWORK_OUT_LOCATION "${CLANGD_FRAMEWORK_LOCATION}/Versions/A") + + # Create the framework info PLIST. + set(CLANGD_XPC_FRAMEWORK_NAME "${name}") + configure_file( + "${CLANGD_XPC_SOURCE_DIR}/cmake/Info.plist.in" + "${CLANGD_XPC_BINARY_DIR}/${name}.Info.plist") + + set(CLANGD_XPC_SERVICE_NAME "clangd") + set(CLANGD_XPC_SERVICE_OUT_LOCATION + "${CLANGD_FRAMEWORK_OUT_LOCATION}/XPCServices/${CLANGD_XPC_SERVICE_NAME}.xpc/Contents") + + # Create the XPC service info PLIST. + set(CLANGD_XPC_SERVICE_BUNDLE_NAME "org.llvm.${CLANGD_XPC_SERVICE_NAME}") + set(CLANGD_PATH_IN_CLANGDXPC_SERVICE "${CLANGD_FRAMEWORK_OUT_LOCATION}/XPCServices/${CLANGD_XPC_SERVICE_NAME}.xpc/Contents/MacOS/clangd") + configure_file( + "${CLANGD_XPC_SOURCE_DIR}/cmake/XPCServiceInfo.plist.in" + "${CLANGD_XPC_BINARY_DIR}/${name}Service.Info.plist") + + # Create the custom command + add_custom_command(OUTPUT ${CLANGD_FRAMEWORK_LOCATION} + # Copy the PLIST. + COMMAND ${CMAKE_COMMAND} -E copy + "${CLANGD_XPC_BINARY_DIR}/${name}.Info.plist" + "${CLANGD_FRAMEWORK_OUT_LOCATION}/Resources/Info.plist" + + # Copy the framework binary. + COMMAND ${CMAKE_COMMAND} -E copy + "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/lib${target}.dylib" + "${CLANGD_FRAMEWORK_OUT_LOCATION}/${name}" + + # Copy the XPC Service PLIST. + COMMAND ${CMAKE_COMMAND} -E copy + "${CLANGD_XPC_BINARY_DIR}/${name}Service.Info.plist" + "${CLANGD_XPC_SERVICE_OUT_LOCATION}/Info.plist" + + # Copy the clangd binary. + COMMAND ${CMAKE_COMMAND} -E copy + "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/clangd" + "${CLANGD_XPC_SERVICE_OUT_LOCATION}/MacOS/clangd" + + # Copy the clangd-xpc-adapter binary. + COMMAND ${CMAKE_COMMAND} -E copy + "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/clangd-xpc-adapter" + "${CLANGD_XPC_SERVICE_OUT_LOCATION}/MacOS/clangd-xpc-adapter" + + COMMAND ${CMAKE_COMMAND} -E create_symlink "A" + "${CLANGD_FRAMEWORK_LOCATION}/Versions/Current" + + COMMAND ${CMAKE_COMMAND} -E create_symlink + "Versions/Current/Resources" + "${CLANGD_FRAMEWORK_LOCATION}/Resources" + + COMMAND ${CMAKE_COMMAND} -E create_symlink + "Versions/Current/XPCServices" + "${CLANGD_FRAMEWORK_LOCATION}/XPCServices" + + COMMAND ${CMAKE_COMMAND} -E create_symlink + "Versions/Current/${name}" + "${CLANGD_FRAMEWORK_LOCATION}/${name}" + + DEPENDS + "${CLANGD_XPC_BINARY_DIR}/${name}.Info.plist" + "${CLANGD_XPC_BINARY_DIR}/${name}Service.Info.plist" + clangd + clangd-xpc-adapter + COMMENT "Creating ClangdXPC framework" + VERBATIM + ) + + add_custom_target( + ClangdXPC + DEPENDS + ${target} + ${CLANGD_FRAMEWORK_LOCATION} + ) + +endmacro(create_clangd_xpc_framework) Index: xpc/framework/CMakeLists.txt =================================================================== --- /dev/null +++ xpc/framework/CMakeLists.txt @@ -0,0 +1,12 @@ + +set(SOURCES + ClangdXPC.cpp) +add_clang_library(ClangdXPCLib SHARED + ${SOURCES} + DEPENDS + clangd + clangd-xpc-adapter +) +create_clangd_xpc_framework(ClangdXPCLib "ClangdXPC") + +add_dependencies(ClangdXPC clangd clangd-xpc-adapter) \ No newline at end of file Index: xpc/framework/ClangdXPC.cpp =================================================================== --- /dev/null +++ xpc/framework/ClangdXPC.cpp @@ -0,0 +1,5 @@ + +/// Returns the bundle identifier of the Clangd XPC service. +extern "C" const char *clangd_xpc_get_bundle_identifier() { + return "org.llvm.clangd"; +} Index: xpc/test-client/CMakeLists.txt =================================================================== --- /dev/null +++ xpc/test-client/CMakeLists.txt @@ -0,0 +1,25 @@ +include_directories( + ${CMAKE_CURRENT_SOURCE_DIR}/../../ +) + +add_clang_tool( + clangd-xpc-test-client + ClangdXPCTestClient.cpp + + DEPENDS ClangdXPC +) + +set(LLVM_LINK_COMPONENTS + support +) + +target_link_libraries(clangd-xpc-test-client + PRIVATE + clangBasic + clangDaemon + clangFormat + clangFrontend + clangSema + clangTooling + clangToolingCore +) Index: xpc/test-client/ClangdXPCTestClient.cpp =================================================================== --- /dev/null +++ xpc/test-client/ClangdXPCTestClient.cpp @@ -0,0 +1,103 @@ +#include "clang/Basic/LLVM.h" +#include "llvm/ADT/SmallString.h" +#include "llvm/Support/LineIterator.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/raw_ostream.h" +#include +#include +#include +#include + +typedef const char *(*clangd_xpc_get_bundle_identifier_t)(void); + +using namespace llvm; +using namespace clang; + +std::string getLibraryPath() { + Dl_info info; + if (dladdr((void *)(uintptr_t)getLibraryPath, &info) == 0) + llvm_unreachable("Call to dladdr() failed"); + llvm::SmallString<128> LibClangPath; + LibClangPath = llvm::sys::path::parent_path( + llvm::sys::path::parent_path(info.dli_fname)); + llvm::sys::path::append(LibClangPath, "lib", "ClangdXPC.framework", + "ClangdXPC"); + return LibClangPath.str(); +} + +static void dumpXPCObject(xpc_object_t Object, llvm::raw_ostream &OS, + std::size_t &count) { + xpc_type_t Type = xpc_get_type(Object); + if (Type == XPC_TYPE_DICTIONARY) { + const char *const payload = xpc_dictionary_get_string(Object, "payload"); + count += strlen(payload); + if (payload == nullptr) { + OS << "[error]: dumpXPCObject(): object missing payload"; + } else { + OS << payload; + } + } else { + OS << "[error]: dumpXPCObject(): object is not a dictionary"; + } +} + +int main(int argc, char *argv[]) { + // Open the ClangdXPC dylib in the framework. + std::string LibPath = getLibraryPath(); + void *dlHandle = dlopen(LibPath.c_str(), RTLD_LOCAL | RTLD_FIRST); + if (!dlHandle) + return 1; + + // Lookup the XPC service bundle name, and launch it. + clangd_xpc_get_bundle_identifier_t clangd_xpc_get_bundle_identifier = + (clangd_xpc_get_bundle_identifier_t)dlsym( + dlHandle, "clangd_xpc_get_bundle_identifier"); + xpc_connection_t conn = xpc_connection_create( + clangd_xpc_get_bundle_identifier(), dispatch_get_main_queue()); + + // Dump the XPC events. + xpc_connection_set_event_handler(conn, ^(xpc_object_t event) { + static std::size_t byte_count = 0; + if (event == XPC_ERROR_CONNECTION_INVALID) { + llvm::errs() << "Received XPC_ERROR_CONNECTION_INVALID."; + llvm::errs() << "total B received: " << byte_count; + exit(EXIT_SUCCESS); + } + if (event == XPC_ERROR_CONNECTION_INTERRUPTED) { + llvm::errs() << "Received XPC_ERROR_CONNECTION_INTERRUPTED."; + llvm::errs() << "total B received: " << byte_count; + exit(EXIT_SUCCESS); + } + + dumpXPCObject(event, llvm::outs(), byte_count); + llvm::outs() << "\n"; + }); + + xpc_connection_resume(conn); + + // Read the input to determine the things to send to clangd. + llvm::ErrorOr> Stdin = + llvm::MemoryBuffer::getSTDIN(); + if (!Stdin) { + llvm::errs() << "Failed to get STDIN!\n"; + return 1; + } + for (llvm::line_iterator It(**Stdin, /*SkipBlanks=*/true, + /*CommentMarker=*/'#'); + !It.is_at_eof(); ++It) { + StringRef Line = *It; + std::vector keys = {"payload", "workspace"}; + std::vector values = {xpc_string_create(Line.str().c_str()), + xpc_string_create("test")}; + + auto request = xpc_dictionary_create(keys.data(), values.data(), 2); + + xpc_connection_send_message(conn, request); + } + + dispatch_main(); + + // dispatch_main() doesn't return + return EXIT_FAILURE; +} Index: xpc/tool/CMakeLists.txt =================================================================== --- /dev/null +++ xpc/tool/CMakeLists.txt @@ -0,0 +1,5 @@ +add_clang_tool(clangd-xpc-adapter + ClangdXpcAdapter.cpp + ClangdWorkspaceInstances.cpp + ClangdSubprocess.cpp + ) \ No newline at end of file Index: xpc/tool/ClangdSubprocess.h =================================================================== --- /dev/null +++ xpc/tool/ClangdSubprocess.h @@ -0,0 +1,35 @@ +#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_XPC_CLANGD_SUBPROCESS_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_XPC_CLANGD_SUBPROCESS_H + +#include "llvm/ADT/Optional.h" +#include +#include + +/// Holds running clangd process data. +/// Lifetime of an instance is not automagically tied to the process. +struct ClangdSubprocess { + const pid_t PID = 0; + const int childStdOut = -1; + const int childStdErr = -1; + const int childStdIn = -1; + + ClangdSubprocess(pid_t PID, int childStdOut, int childStdErr, int childStdIn); + + bool writeLSPMessage(const std::string &msg); + + llvm::Optional readLSPMessage(); + + llvm::Optional readStdErr(); +}; + +llvm::Optional +startClangdSubprocess(const std::string &path, + const std::vector &args); + +// Use with care - unless someone calls waitpid() the process would become a +// zombie. +void sendKillSignal(pid_t pid); + +void endClangdSubprocess(pid_t pid); + +#endif \ No newline at end of file Index: xpc/tool/ClangdSubprocess.cpp =================================================================== --- /dev/null +++ xpc/tool/ClangdSubprocess.cpp @@ -0,0 +1,338 @@ +#include "ClangdSubprocess.h" + +#include "log.h" + +#include +#include + +ClangdSubprocess::ClangdSubprocess(pid_t PID, int childStdOut, int childStdErr, + int childStdIn) + : PID(PID), childStdOut(childStdOut), childStdErr(childStdErr), + childStdIn(childStdIn) {} + +bool ClangdSubprocess::writeLSPMessage(const std::string &msg) { + log_debug() << "ClangdSubprocess::writeLSPMessage() started"; + std::size_t alreadyWritten = 0; + ssize_t lastWritten = 0; + while (true) { + if (alreadyWritten == msg.size()) { + break; + } else if (alreadyWritten > msg.size()) { + log() << "ClangdSubprocess::writeLSPMessage() error 'already_written > " + "msg.size()'"; + return false; + } + + lastWritten = ::write(childStdIn, msg.c_str() + alreadyWritten, + msg.size() - alreadyWritten); + if (lastWritten == -1) { + if (errno == EINTR) + continue; + log() << "ClangdSubprocess::writeLSPMessage() failed"; + return false; + } + + alreadyWritten += lastWritten; + }; + + log_debug() << "ClangdSubprocess::writeLSPMessage() successfully finished"; + return true; +} + +/// Returns true iff successfully read prefix from file discriptor fd. +static bool ignorePrefix(const int fd, const std::string &prefix) { + log_debug() << "ignorePrefix(): started"; + std::vector buffer(prefix.size()); + std::size_t remainingPrefixLength = prefix.size(); + std::string actualPrefix; + + while (remainingPrefixLength > 0) { + const int bytesRead = ::read(fd, buffer.data(), remainingPrefixLength); + if (bytesRead < 0) { + if (errno == EINTR) + continue; + + if (errno == EAGAIN) + log_debug() << "ignorePrefix() errno=EAGAIN"; + + log() << "ignorePrefix(): failed to read()"; + return false; + } else if (bytesRead > 0) { + actualPrefix += std::string(buffer.begin(), buffer.begin() + bytesRead); + remainingPrefixLength -= bytesRead; + log_debug() << "ignorePrefix(): read() returned " << bytesRead + << "B; remainingPrefixLength=" << remainingPrefixLength + << "B"; + } else { + log() << "ignorePrefix(): failed to read() more bytes"; + return false; + } + } + + if (actualPrefix != prefix) { + log() << "ignorePrefix(): expected '" << prefix << "'; got '" + << actualPrefix << "'"; + return false; + } + + log_debug() << "ignorePrefix(): success"; + return true; +} + +/// Doesn't return the terminator char at the end of result. +static llvm::Optional readUntilChar(int fd, const char terminator, + std::size_t hardLimit) { + log_debug() << "readUntilChar(): started"; + std::string result; + char ch; + while (hardLimit > 0) { + const int bytesRead = ::read(fd, &ch, 1); + if (bytesRead == 1) { + log_debug() << "readUntilChar(): read 1B"; + if (ch == terminator) + return result; + result += ch; + --hardLimit; + continue; + } else if (bytesRead < 0) { + if (errno == EINTR) + continue; + + if (errno == EAGAIN) + log_debug() << "readUntilChar() errno=EAGAIN"; + + log() << "readUntilChar(): failed to read()"; + return llvm::None; + } else { + log() << "readUntilChar(): failed to read() more bytes"; + return llvm::None; + } + } + log() << "readUntilChar(): failed to reach terminator char before hardLimit " + "of " + << hardLimit << "B"; + return llvm::None; +} + +/// returns JSON payload length +static llvm::Optional parseLSPHeader(const int fd) { + log_debug() << "parseLSPHeader(): started"; + // FIXME This is just simplistic - expects LSP header to be always just single + // LSP header file (Content-Length). + // TODO Proper LSP header support + std::size_t contentLength = 0; + + // The first and only expected LSP header field. + { + if (!ignorePrefix(fd, "Content-Length: ")) { + log() << "parseLSPHeader(): expected LSP header field start - " + "'Content-Length: ' missing"; + return llvm::None; + } + + llvm::Optional tentativeContentLength = + readUntilChar(fd, '\r', 10); + if (tentativeContentLength == llvm::None) { + log() << "parseLSPHeader(): expected LSP header field Content-Length " + "value " + "missing"; + return llvm::None; + } + + for (char c : tentativeContentLength.getValue()) { + if (c < '0' || c > '9') { + break; + } + contentLength *= 10; + contentLength += c - '0'; + } + + if (!ignorePrefix(fd, "\n")) { + log() << "parseLSPHeader(): expected LSP header field delimiter - \\r\\n " + "missing"; + return llvm::None; + } + } + + if (!ignorePrefix(fd, "\r\n")) { + log() << "parseLSPHeader(): expected LSP header delimiter - \\r\\n missing"; + return llvm::None; + } + + return contentLength; +} + +// TODO OPTIMIZE +llvm::Optional ClangdSubprocess::readLSPMessage() { + log_debug() << "ClangdSubprocess::readLSPMessage() started"; + std::size_t expectedContentLength = 0; + { + llvm::Optional tentativeContentLength = + parseLSPHeader(childStdOut); + if (!tentativeContentLength) { + log() << "ClangdSubprocess::readLSPMessage() failed to read LSP header"; + return llvm::None; + } + expectedContentLength = tentativeContentLength.getValue(); + } + + log() << "ClangdSubprocess::readLSPMessage() received LSP header for " + << expectedContentLength << "B JSON content"; + + std::vector buffer(expectedContentLength); + std::size_t alreadyReadByteCount = 0; + + log_debug() + << "ClangdSubprocess::readLSPMessage() childStdOut file descriptor = " + << childStdOut; + while (alreadyReadByteCount < expectedContentLength) { + const int bytesRead = + ::read(childStdOut, buffer.data() + alreadyReadByteCount, + expectedContentLength - alreadyReadByteCount); + if (bytesRead > 0) { + alreadyReadByteCount += bytesRead; + log_debug() << "ClangdSubprocess::readLSPMessage() read " << bytesRead + << "B; remaining " + << expectedContentLength - alreadyReadByteCount << " B"; + } else if (bytesRead < 0) { + if (errno == EINTR) + continue; + + if (errno == EAGAIN) + log_debug() << "readLSPMessage() errno=EAGAIN"; + + log() << "ClangdSubprocess::readLSPMessage() failed"; + return llvm::None; + } else { + log() << "ClangdSubprocess::readLSPMessage() returned 0B"; + return llvm::None; + } + } + + log_debug() << "ClangdSubprocess::readLSPMessage() successfully finished"; + return std::string(buffer.begin(), buffer.end()); +} + +llvm::Optional ClangdSubprocess::readStdErr() { + log_debug() << "ClangdSubprocess::readStdErr() started; childStdErr file " + "descriptor = " + << childStdErr; + std::string result; + + std::vector buffer(4 * 1024); // 4kB + while (true) { + const int bytesRead = ::read(childStdErr, buffer.data(), buffer.size()); + if (bytesRead > 0) { + result += std::string(buffer.begin(), buffer.end()); + log_debug() << "ClangdSubprocess::readStdErr() read " << bytesRead << "B"; + } else if (bytesRead < 0) { + if (errno == EINTR) { + log_debug() << "ClangdSubprocess::readStdErr() errno=EINTR"; + continue; + } + if (errno == EAGAIN) { + log_debug() << "ClangdSubprocess::readStdErr() errno=EAGAIN"; + break; + } + + log() << "ClangdSubprocess::readStdErr() failed"; + log() << "ClangdSubprocess::readStdErr() incomplete result='" << result + << "'"; + return llvm::None; + } else { + log() << "ClangdSubprocess::readStdErr() returned 0B"; + log() << "ClangdSubprocess::readStdErr() incomplete result='" << result + << "'"; + return llvm::None; + } + } + + log_debug() << "ClangdSubprocess::readStdErr() successfully finished"; + return result; +} + +llvm::Optional +startClangdSubprocess(const std::string &path, + const std::vector &args) { + log_debug() << "startClangdSubprocess(): started"; + + pid_t pid = 0; + int childStdOut = -1; + int childStdErr = -1; + int childStdIn = -1; + + posix_spawn_file_actions_t action; + + int childInputPipe[2] = {-1, -1}; // read end, write end + int childOutputPipe[2] = {-1, -1}; // read end, write end + int childErrorPipe[2] = {-1, -1}; // read end, write end + + if (pipe(childInputPipe) || pipe(childOutputPipe) || pipe(childErrorPipe)) + return llvm::None; + + childStdOut = childOutputPipe[0]; + childStdErr = childErrorPipe[0]; + childStdIn = childInputPipe[1]; + const int childWrite = childOutputPipe[1]; + const int childError = childErrorPipe[1]; + const int childRead = childInputPipe[0]; + + if (posix_spawn_file_actions_init(&action) || + posix_spawn_file_actions_addclose(&action, childStdIn) || + posix_spawn_file_actions_addclose(&action, childStdOut) || + posix_spawn_file_actions_addclose(&action, childStdErr) || + posix_spawn_file_actions_adddup2(&action, childRead, STDIN_FILENO) || + posix_spawn_file_actions_adddup2(&action, childWrite, STDOUT_FILENO) || + posix_spawn_file_actions_adddup2(&action, childError, STDERR_FILENO) || + posix_spawn_file_actions_addclose(&action, childRead) || + posix_spawn_file_actions_addclose(&action, childWrite) || + posix_spawn_file_actions_addclose(&action, childError)) { + log() << "startClangdSubprocess(): posix_spawn_file_actions_t setup failed"; + return llvm::None; + } + + char **args_ptrs = new char *[args.size() + 1 + 1]; + + args_ptrs[0] = const_cast(path.c_str()); // TODO FIXME const_cast + + std::size_t i = 1; + for (auto &a : args) { + args_ptrs[i] = const_cast(a.c_str()); // TODO FIXME const_cast + ++i; + } + args_ptrs[args.size() + 1] = nullptr; + + log_debug() << "startClangdSubprocess(): path = '" << path << "'"; + + if (posix_spawn(&pid, /* path */ path.c_str(), &action, /* attrp */ nullptr, + /* argv */ args_ptrs, /* envp */ nullptr)) { + log() << "startClangdSubprocess(): failed"; + return llvm::None; + } else { + log() << "startClangdSubprocess(): spawned PID " << pid; + } + + fcntl(childStdOut, F_SETFL, O_NONBLOCK); + fcntl(childStdErr, F_SETFL, O_NONBLOCK); + + log_info() << "startClangdSubprocess(): switched file descriptor " + "of child's stdout and stderr to O_NONBLOCK"; + + posix_spawn_file_actions_destroy(&action); + delete[] args_ptrs; + + return ClangdSubprocess(pid, childStdOut, childStdErr, childStdIn); +} + +// Use with care - unless someone calls waitpid() the process would become a +// zombie. +void sendKillSignal(pid_t pid) { + log() << "sendKillSignal(): PID " << pid; + kill(pid, SIGKILL); +} + +void endClangdSubprocess(pid_t pid) { + log() << "endClangdSubprocess(): PID " << pid; + kill(pid, SIGKILL); + waitpid(pid, 0, WNOHANG); +} \ No newline at end of file Index: xpc/tool/ClangdWorkspaceInstances.h =================================================================== --- /dev/null +++ xpc/tool/ClangdWorkspaceInstances.h @@ -0,0 +1,84 @@ +#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_XPC_CLANGD_WORKSPACE_INSTANCES_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_XPC_CLANGD_WORKSPACE_INSTANCES_H + +#include +#include +#include +#include +#include +#include + +#include "llvm/ADT/Optional.h" + +#include + +#include "ClangdSubprocess.h" + +class ClangdWorkspaceInstances { +public: + ClangdWorkspaceInstances(const std::string &path, + const std::vector &args); + + ~ClangdWorkspaceInstances(); + + // Must be called before any createInstance() + void setStdOutCallback(std::function + callback); + + // Must be called before any createInstance() + void setStdErrCallback( + std::function + callback); + + void setOnSpawnCallback( + std::function callback); + void setOnRemoveCallback( + std::function callback); + + /// returns true if successfull + bool createInstance(const std::string &workspace); + + bool removeInstance(const pid_t pid); + + /// returns true iff successful + bool restartInstance(const pid_t pid); + + bool writeLSPMessage(const std::string &workspace, const std::string &msg); + + llvm::Optional readLSPMessage(const std::string &workspace); + + llvm::Optional readStdErr(const std::string &workspace); + + bool doesInstanceExist(const std::string &workspace); + + std::size_t existingInstancesCount(); + +private: + const std::string path; + const std::vector args; + + std::recursive_mutex mut; + + // using list to have stable iterators, performance is not an issue (expected + // number of workspaces is very low) + std::list processes; + std::map::iterator> PIDToInstance; + std::map workspaceToPID; + std::map PIDToWorkspace; + + std::map PIDToStdOutHandler; + std::map PIDToStdErrHandler; + + std::function onSpawnCallback; + std::function onRemoveCallback; + std::function + stdOutCallback; + std::function + stdErrCallback; +}; + +#endif \ No newline at end of file Index: xpc/tool/ClangdWorkspaceInstances.cpp =================================================================== --- /dev/null +++ xpc/tool/ClangdWorkspaceInstances.cpp @@ -0,0 +1,378 @@ +#include "ClangdWorkspaceInstances.h" + +#include "log.h" + +ClangdWorkspaceInstances::ClangdWorkspaceInstances( + const std::string &path, const std::vector &args) + : path(path), args(args) {} + +ClangdWorkspaceInstances::~ClangdWorkspaceInstances() { + log_debug() + << "ClangdWorkspaceInstances::~ClangdWorkspaceInstances(): started"; + std::lock_guard lock(mut); + log_debug() << "ClangdWorkspaceInstances::~ClangdWorkspaceInstances(): mutex " + "acquired"; + + for (const auto it : PIDToInstance) + removeInstance(it.first); +} + +void ClangdWorkspaceInstances::setStdOutCallback( + std::function + callback) { + log_debug() << "ClangdWorkspaceInstances::setStdOutCallback(): started"; + std::lock_guard lock(mut); + log_debug() + << "ClangdWorkspaceInstances::setStdOutCallback(): mutex acquired"; + stdOutCallback = callback; +} + +void ClangdWorkspaceInstances::setStdErrCallback( + std::function + callback) { + log_debug() << "ClangdWorkspaceInstances::setStdErrCallback(): started"; + std::lock_guard lock(mut); + log_debug() + << "ClangdWorkspaceInstances::setStdErrCallback(): mutex acquired"; + stdErrCallback = callback; +} + +void ClangdWorkspaceInstances::setOnSpawnCallback( + std::function callback) { + log_debug() << "ClangdWorkspaceInstances::setOnSpawnCallback(): started"; + std::lock_guard lock(mut); + log_debug() + << "ClangdWorkspaceInstances::setOnSpawnCallback(): mutex acquired"; + onSpawnCallback = callback; +} + +void ClangdWorkspaceInstances::setOnRemoveCallback( + std::function callback) { + log_debug() << "ClangdWorkspaceInstances::setOnRemoveCallback(): started"; + std::lock_guard lock(mut); + log_debug() + << "ClangdWorkspaceInstances::setOnRemoveCallback(): mutex acquired"; + onRemoveCallback = callback; +} + +bool ClangdWorkspaceInstances::createInstance(const std::string &workspace) { + log_debug() << "ClangdWorkspaceInstances::createInstance(): started " + "workspace='" + << workspace << "'"; + std::lock_guard lock(mut); + log_debug() << "ClangdWorkspaceInstances::createInstance(): mutex " + "acquired"; + + if (doesInstanceExist(workspace)) { + log_info() << "ClangdWorkspaceInstances::createInstance(): workspace " + "already exists - returning early"; + return true; + } + { + llvm::Optional child = startClangdSubprocess(path, args); + if (child == llvm::None) + return false; + processes.push_back(child.getValue()); + } + const pid_t PID = processes.back().PID; + log_info() << "ClangdWorkspaceInstances::createInstance(): spawned " + "subprocess; PID=" + << PID; + PIDToInstance.emplace(PID, --processes.end()); + workspaceToPID.emplace(workspace, PID); + PIDToWorkspace.emplace(PID, workspace); + + // asynchronous handling of child's stdout + { + dispatch_source_t eventSource = dispatch_source_create( + DISPATCH_SOURCE_TYPE_READ, PIDToInstance[PID]->childStdOut, 0, + dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)); + dispatch_source_set_event_handler(eventSource, ^{ + log_debug() << "stdout handler: started"; + std::lock_guard lock(mut); + log_debug() << "stdout handler: mutex acquired"; + + const auto workspaceIt = PIDToWorkspace.find(PID); + if (workspaceIt == PIDToWorkspace.end()) { + log() << "stdout handler: nonexistent workspace for PID " << PID; + sendKillSignal(PID); + } + const auto workspace = workspaceIt->second; + + const llvm::Optional response = readLSPMessage(workspace); + if (response != llvm::None) { + log_debug() << "stdout handler: calling stdOutCallback"; + stdOutCallback(workspace, response.getValue()); + } else { + log() << "stdout handler: failed to read from stdout of child PID " + << PID; + sendKillSignal(PID); + } + }); + + dispatch_resume(eventSource); + log_debug() << "ClangdWorkspaceInstances::createInstance(): created " + "dispatch source for stdout of PID " + << PID; + PIDToStdOutHandler[PID] = eventSource; + } + + { + // Using QOS_CLASS_BACKGROUND as we are just logging stderr output. + dispatch_source_t eventSource = dispatch_source_create( + DISPATCH_SOURCE_TYPE_READ, PIDToInstance[PID]->childStdErr, 0, + dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)); + dispatch_source_set_event_handler(eventSource, ^{ + log_debug() << "stderr handler: started"; + std::lock_guard lock(mut); + log_debug() << "stderr handler: mutex acquired"; + + const auto workspaceIt = PIDToWorkspace.find(PID); + if (workspaceIt == PIDToWorkspace.end()) { + log() << "stderr handler: nonexistent workspace for PID " << PID; + sendKillSignal(PID); + } + const auto workspace = workspaceIt->second; + + const llvm::Optional response = readStdErr(workspace); + if (response != llvm::None) { + log_debug() << "stderr handler: calling stdErrCallback"; + stdErrCallback(workspace, PID, response.getValue()); + } else { + log() << "stdout handler: failed to read from stderr of child PID " + << PID; + sendKillSignal(PID); + } + }); + + dispatch_resume(eventSource); + log_debug() << "ClangdWorkspaceInstances::createInstance(): created " + "dispatch source for stderr of PID " + << PID; + PIDToStdErrHandler[PID] = eventSource; + } + onSpawnCallback(workspace); + + return true; +} + +bool ClangdWorkspaceInstances::removeInstance(const pid_t pid) { + log_debug() << "ClangdWorkspaceInstances::removeInstance(): started"; + std::lock_guard lock(mut); + log_debug() << "ClangdWorkspaceInstances::removeInstance(): mutex acquired"; + + const auto childIt = PIDToInstance.find(pid); + // Try to do the clean-up no matter how messy the current state is and + // eventually return failure only after that. + bool anyFailure = false; + + if (childIt != PIDToInstance.end()) { + // Must precede closing child's I/O pipes file descriptors. + { + const auto stdoutHandlerIt = PIDToStdOutHandler.find(pid); + if (stdoutHandlerIt != PIDToStdOutHandler.end()) { + dispatch_source_cancel(stdoutHandlerIt->second); + } else { + log_info() << "removeInstance(): didn't find stdout handler for PID " + << pid; + anyFailure = true; + } + } + { + const auto stderrHandlerIt = PIDToStdErrHandler.find(pid); + if (stderrHandlerIt != PIDToStdErrHandler.end()) { + dispatch_source_cancel(stderrHandlerIt->second); + } else { + log_info() << "removeInstance(): didn't find stderr handler for PID " + << pid; + anyFailure = true; + } + } + + endClangdSubprocess(pid); + processes.erase(childIt->second); + if (PIDToInstance.erase(pid) != 1) { + log() << "ClangdWorkspaceInstances::removeInstance(): PIDToInstance " + "doesn't " + "contain PID " + << pid; + anyFailure = true; + } + + std::string workspace; + { + const auto workspaceIt = PIDToWorkspace.find(pid); + if (workspaceIt == PIDToWorkspace.end()) { + log() << "ClangdWorkspaceInstances::removeInstance(): PIDToWorkspace " + "doesn't contain PID " + << pid; + anyFailure = true; + } + workspace = workspaceIt->second; + onRemoveCallback(workspace); + } + + if (workspaceToPID.erase(workspace) != 1) { + log() << "ClangdWorkspaceInstances::removeInstance(): workspaceToPID " + "doesn't " + "contain workspace " + << workspace; + anyFailure = true; + } + + if (PIDToWorkspace.erase(pid) != 1) { + log() << "ClangdWorkspaceInstances::removeInstance(): PIDToWorkspace " + "doesn't " + "contain PID " + << pid; + anyFailure = true; + } + } else { + log() << "ClangdWorkspaceInstances::removeInstance(): PID " << pid + << " doesn't exist anymore"; + anyFailure = true; + } + + return !anyFailure; +} + +/// returns true iff successful +bool ClangdWorkspaceInstances::restartInstance(const pid_t PID) { + log_debug() << "ClangdWorkspaceInstances::restartInstance(): started"; + std::lock_guard lock(mut); + log_debug() << "ClangdWorkspaceInstances::restartInstance(): mutex acquired"; + + // Must be done before instance is removed. + llvm::Optional workspace = [&]() { + const auto workspaceIt = PIDToWorkspace.find(PID); + return workspaceIt == PIDToWorkspace.end() + ? llvm::Optional(llvm::None) + : workspaceIt->second; + }(); + + if (!removeInstance(PID)) { + log() << "ClangdWorkspaceInstances::restartInstance(): failed to remove " + "child"; + return false; + } + + if (workspace == llvm::None) { + log() << "ClangdWorkspaceInstances::restartInstance(): no instance for PID " + << PID; + return false; + } + + if (!createInstance(workspace.getValue())) { + log() + << "ClangdWorkspaceInstances::restartInstance(): failed to start child"; + return false; + } + + return true; +} + +bool ClangdWorkspaceInstances::writeLSPMessage(const std::string &workspace, + const std::string &msg) { + log_debug() << "ClangdWorkspaceInstances::writeLSPMessage(): started"; + std::lock_guard lock(mut); + log_debug() << "ClangdWorkspaceInstances::writeLSPMessage(): mutex acquired"; + + auto pidIt = workspaceToPID.find(workspace); + if (pidIt == workspaceToPID.end()) { + log() << "ClangdWorkspaceInstances::writeLSPMessage(): workspaceToPID " + "doesn't " + "contain workspace " + << workspace; + return false; + } + + auto processIt = PIDToInstance.find(pidIt->second); + if (processIt == PIDToInstance.end()) { + log() + << "ClangdWorkspaceInstances::writeLSPMessage(): PIDToInstance doesn't " + "contain PID " + << pidIt->second; + return false; + } + + return processIt->second->writeLSPMessage(msg); +} + +llvm::Optional +ClangdWorkspaceInstances::readLSPMessage(const std::string &workspace) { + log_debug() << "ClangdWorkspaceInstances::readLSPMessage(): started"; + std::lock_guard lock(mut); + log_debug() << "ClangdWorkspaceInstances::readLSPMessage(): mutex " + "acquired"; + + auto pidIt = workspaceToPID.find(workspace); + if (pidIt == workspaceToPID.end()) { + log() << "ClangdWorkspaceInstances::readLSPMessage(): no " + "child for " + "workspace '" + << workspace << "'"; + return llvm::None; + } + + auto procIt = PIDToInstance.find(pidIt->second); + if (procIt == PIDToInstance.end()) { + log() << "ClangdWorkspaceInstances::readLSPMessage(): no " + "sub-process for workspace '" + << workspace << "'"; + return llvm::None; + } + + log_debug() << "ClangdWorkspaceInstances::readLSPMessage(): success"; + return procIt->second->readLSPMessage(); +} + +llvm::Optional +ClangdWorkspaceInstances::readStdErr(const std::string &workspace) { + log_debug() << "ClangdWorkspaceInstances::readStdErr(): started"; + std::lock_guard lock(mut); + log_debug() << "ClangdWorkspaceInstances::readStdErr(): mutex acquired"; + + auto pidIt = workspaceToPID.find(workspace); + if (pidIt == workspaceToPID.end()) { + log() << "ClangdWorkspaceInstances::readStdErr(): no child for " + "workspace '" + << workspace << "'"; + return llvm::None; + } + + auto procIt = PIDToInstance.find(pidIt->second); + if (procIt == PIDToInstance.end()) { + log() << "ClangdWorkspaceInstances::readStdErr(): no sub-process " + "for workspace '" + << workspace << "'"; + return llvm::None; + } + + llvm::Optional result = procIt->second->readStdErr(); + if (result != llvm::None) + log_debug() << "ClangdWorkspaceInstances::readStdErr(): success"; + else + log_debug() << "ClangdWorkspaceInstances::readStdErr(): failed"; + + return result; +} + +bool ClangdWorkspaceInstances::doesInstanceExist(const std::string &workspace) { + log_debug() << "ClangdWorkspaceInstances::doesInstanceExist(): started"; + std::lock_guard lock(mut); + log_debug() << "ClangdWorkspaceInstances::doesInstanceExist(): mutex " + "acquired"; + + return workspaceToPID.find(workspace) != workspaceToPID.end(); +} + +std::size_t ClangdWorkspaceInstances::existingInstancesCount() { + log_debug() << "ClangdWorkspaceInstances::existingInstancesCount(): started"; + std::lock_guard lock(mut); + log_debug() << "ClangdWorkspaceInstances::existingInstancesCount(): mutex " + "acquired"; + + return processes.size(); +} \ No newline at end of file Index: xpc/tool/ClangdXpcAdapter.cpp =================================================================== --- /dev/null +++ xpc/tool/ClangdXpcAdapter.cpp @@ -0,0 +1,172 @@ +#include +#include + +#include "ClangdWorkspaceInstances.h" +#include "log.h" + +namespace { +ClangdWorkspaceInstances *clangdInstancesPtr = nullptr; +} + +#include + +namespace { +void connectionHandler(xpc_connection_t clientConnection) { + + xpc_connection_set_target_queue( + clientConnection, dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)); + + clangdInstancesPtr->setStdOutCallback([clientConnection]( + const std::string &workspace, + const std::string &output) { + log_debug() << "clangd children stdout callback: started for workspace='" + << workspace << "'"; + + // TODO OPTIMIZE - use static const char** + std::vector keys = {"payload", "workspace"}; + std::vector values = {xpc_string_create(output.c_str()), + xpc_string_create(workspace.c_str())}; + + auto response = xpc_dictionary_create(keys.data(), values.data(), 2); + + xpc_connection_send_message(clientConnection, response); + + xpc_release(response); + }); + + xpc_connection_set_event_handler(clientConnection, ^(xpc_object_t message) { + if (message == XPC_ERROR_CONNECTION_INVALID) { + // connection is being terminated + log() << "XPC event handler: received XPC_ERROR_CONNECTION_INVALID"; + return; + } + + if (xpc_get_type(message) != XPC_TYPE_DICTIONARY) { + log() << "XPC event handler: received XPC message of unknown type"; + return; + } + + const char *const payload = xpc_dictionary_get_string(message, "payload"); + const char *const workspace = + xpc_dictionary_get_string(message, "workspace"); + + if (payload && workspace) { + if (!clangdInstancesPtr->doesInstanceExist(workspace)) { + // No point in locking any mutex here - the instance can always crash. + log_info() + << "XPC event handler: spawning new clangd instance for workspace '" + << workspace << "'"; + if (!clangdInstancesPtr->createInstance(workspace)) { + log() << "XPC event handler: failed to spawn new clangd instance for " + "workspace '" + << "'"; + xpc_connection_cancel(clientConnection); + return; + } + } + + std::string delimitedPayload(payload); + delimitedPayload += "\n---\n"; + + if (!clangdInstancesPtr->writeLSPMessage(workspace, delimitedPayload)) { + log() << "XPC event handler: failed to write_to_child"; + xpc_connection_cancel(clientConnection); + return; + } + } else { + // Just log the invalide message and go on. + __block std::string keysDump; + xpc_dictionary_apply(message, + ^bool(const char *key, xpc_object_t /* ignore */) { + if (!keysDump.empty()) + keysDump = keysDump + ", "; + keysDump = keysDump + "'" + key + "'"; + return true; + }); + log() << "XPC event handler: received invalid XPC message with keys {" + << keysDump << "}"; + } + }); + + xpc_connection_resume(clientConnection); +} +} // namespace + +int main() { + log_debug() << "main(): started"; + + const char *const clangdPath = getenv("CLANGD_PATH"); + + if (!clangdPath) { + log() << "main(): missing CLANGD_PATH environment variable"; + return EXIT_FAILURE; + } + + // TODO FIXME add proper JSON RPC header and don't use delimited mode + ClangdWorkspaceInstances clangdInstances(clangdPath, + {"-input-style=delimited"}); + clangdInstancesPtr = &clangdInstances; + + // logging children stderr + clangdInstancesPtr->setStdErrCallback([](const std::string &workspace, + const pid_t pid, + const std::string &output) { + log_debug() << "clangd child [PID " << pid << "] stderr callback: started"; + log() << "clangd child [PID " << pid << "] stderr callback: workspace='" + << workspace << "' stderr='" << output << "'"; + }); + + // handling XPC transactions + clangdInstancesPtr->setOnSpawnCallback([](const std::string &workspace) { + xpc_transaction_begin(); + log_info() << "ClangdWorkspaceInstances:: onspawn callback: ended XPC " + "transaction for workspace " + << workspace; + }); + clangdInstancesPtr->setOnRemoveCallback([](const std::string &workspace) { + xpc_transaction_end(); + log_info() << "ClangdWorkspaceInstances:: onremove callback: ended XPC " + "transaction for workspace " + << workspace; + }); + + // handle children crashing + { + // We are gonna use GCD for SIGCHLD handling so we can actually do something + // without being overly restricted by signal safety requirements. + // Using DISPATCH_QUEUE_PRIORITY_HIGH as if we detect crash we want to react + // ASAP so user experience impact is as low as possible. + dispatch_source_t source = dispatch_source_create( + DISPATCH_SOURCE_TYPE_SIGNAL, SIGCHLD, 0, + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)); + dispatch_source_set_event_handler(source, ^{ + log_debug() << "signal pseudo-handler: started"; + pid_t pid; + int status; + + while ((pid = waitpid(-1, &status, 0)) > 0) { + if (WIFEXITED(status) && WEXITSTATUS(status) == EXIT_SUCCESS) { + log() << "signal pseudo-handler: clangd PID " << pid + << " exited with EXIT_SUCCESS"; + clangdInstancesPtr->removeInstance(pid); + + if (clangdInstancesPtr->existingInstancesCount() == 0) { + log() << "signal pseudo-handler: no active children - exiting"; + exit(EXIT_SUCCESS); + } + } else { + // TODO FIXME some restarting strategy - maybe some increased and/or + // random delay? + log() << "signal pseudo-handler: clangd PID " << pid + << " died - restarting"; + clangdInstancesPtr->restartInstance(pid); + } + } + }); + dispatch_resume(source); + } + + xpc_main(connectionHandler); + + return EXIT_SUCCESS; +} Index: xpc/tool/log.h =================================================================== --- /dev/null +++ xpc/tool/log.h @@ -0,0 +1,49 @@ +#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_XPC_LOG_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_XPC_LOG_H + +#include +#include + +#include + +class Logger { +private: + enum class log_type { log, info, debug }; + log_type type; + std::string buffer_data; + std::stringstream buffer; + Logger(log_type type) : type(type), buffer_data(), buffer(buffer_data) {} + +public: + template Logger &operator<<(const T &in) { + buffer << in; + return *this; + } + friend Logger log(); + friend Logger log_info(); + friend Logger log_debug(); + ~Logger() { + switch (type) { + case log_type::log: + os_log(OS_LOG_DEFAULT, "%s", buffer.str().c_str()); + break; + case log_type::info: + os_log_info(OS_LOG_DEFAULT, "%s", buffer.str().c_str()); + break; + case log_type::debug: + os_log_debug(OS_LOG_DEFAULT, "%s", buffer.str().c_str()); + break; + } + } + Logger(Logger &&) = default; + + Logger(const Logger &) = delete; + Logger &operator=(const Logger &) = delete; + Logger &operator=(Logger &&) = delete; +}; + +inline Logger log() { return Logger(Logger::log_type::log); } +inline Logger log_info() { return Logger(Logger::log_type::info); } +inline Logger log_debug() { return Logger(Logger::log_type::debug); } + +#endif \ No newline at end of file