From c74698cb75b9923517f7f87eacf04ca0eefc8e72 Mon Sep 17 00:00:00 2001 From: Kyle Edwards Date: Tue, 16 Apr 2019 17:58:02 -0400 Subject: cmUVStreambuf: Add std::streambuf implementation for uv_stream_t This will allow std::istream/std::ostream-based interaction with processes spawned by libuv. --- Source/CMakeLists.txt | 1 + Source/cmUVStreambuf.h | 219 ++++++++++++++++++ Tests/CMakeLib/CMakeLists.txt | 4 +- Tests/CMakeLib/testUVStreambuf.cxx | 457 +++++++++++++++++++++++++++++++++++++ 4 files changed, 680 insertions(+), 1 deletion(-) create mode 100644 Source/cmUVStreambuf.h create mode 100644 Tests/CMakeLib/testUVStreambuf.cxx diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 6fa046c..01c6cd7 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -388,6 +388,7 @@ set(SRCS cmUuid.cxx cmUVHandlePtr.cxx cmUVHandlePtr.h + cmUVStreambuf.h cmUVSignalHackRAII.h cmVariableWatch.cxx cmVariableWatch.h diff --git a/Source/cmUVStreambuf.h b/Source/cmUVStreambuf.h new file mode 100644 index 0000000..29e4fde --- /dev/null +++ b/Source/cmUVStreambuf.h @@ -0,0 +1,219 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#ifndef cmUVStreambuf_h +#define cmUVStreambuf_h + +#include "cmUVHandlePtr.h" + +#include "cm_uv.h" + +#include +#include +#include +#include + +/* + * This file is based on example code from: + * + * http://www.voidcn.com/article/p-vjnlygmc-gy.html + * + * The example code was distributed under the following license: + * + * Copyright 2007 Edd Dawson. + * Distributed under the Boost Software License, Version 1.0. + * + * Boost Software License - Version 1.0 - August 17th, 2003 + * + * Permission is hereby granted, free of charge, to any person or organization + * obtaining a copy of the software and accompanying documentation covered by + * this license (the "Software") to use, reproduce, display, distribute, + * execute, and transmit the Software, and to prepare derivative works of the + * Software, and to permit third-parties to whom the Software is furnished to + * do so, all subject to the following: + * + * The copyright notices in the Software and this entire statement, including + * the above license grant, this restriction and the following disclaimer, + * must be included in all copies of the Software, in whole or in part, and + * all derivative works of the Software, unless such copies or derivative + * works are solely in the form of machine-executable object code generated by + * a source language processor. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT + * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE + * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +template > +class cmBasicUVStreambuf : public std::basic_streambuf +{ +public: + cmBasicUVStreambuf(std::size_t bufSize = 256, std::size_t putBack = 8); + ~cmBasicUVStreambuf() override; + + bool is_open() const; + + cmBasicUVStreambuf* open(uv_stream_t* stream); + + cmBasicUVStreambuf* close(); + +protected: + typename cmBasicUVStreambuf::int_type underflow() override; + std::streamsize showmanyc() override; + + // FIXME: Add write support + +private: + uv_stream_t* Stream = nullptr; + void* OldStreamData; + const std::size_t PutBack; + std::vector InputBuffer; + bool EndOfFile; + + void StreamReadStartStop(); + + void StreamRead(ssize_t nread); + void HandleAlloc(uv_buf_t* buf); +}; + +template +cmBasicUVStreambuf::cmBasicUVStreambuf(std::size_t bufSize, + std::size_t putBack) + : PutBack(std::max(putBack, 1)) + , InputBuffer(std::max(this->PutBack, bufSize) + this->PutBack) +{ + this->close(); +} + +template +cmBasicUVStreambuf::~cmBasicUVStreambuf() +{ + this->close(); +} + +template +bool cmBasicUVStreambuf::is_open() const +{ + return this->Stream != nullptr; +} + +template +cmBasicUVStreambuf* cmBasicUVStreambuf::open( + uv_stream_t* stream) +{ + this->close(); + this->Stream = stream; + this->EndOfFile = false; + if (this->Stream) { + this->OldStreamData = this->Stream->data; + this->Stream->data = this; + } + this->StreamReadStartStop(); + return this; +} + +template +cmBasicUVStreambuf* cmBasicUVStreambuf::close() +{ + if (this->Stream) { + uv_read_stop(this->Stream); + this->Stream->data = this->OldStreamData; + } + this->Stream = nullptr; + CharT* readEnd = this->InputBuffer.data() + this->InputBuffer.size(); + this->setg(readEnd, readEnd, readEnd); + return this; +} + +template +typename cmBasicUVStreambuf::int_type +cmBasicUVStreambuf::underflow() +{ + if (!this->is_open()) { + return Traits::eof(); + } + + if (this->gptr() < this->egptr()) { + return Traits::to_int_type(*this->gptr()); + } + + this->StreamReadStartStop(); + while (this->in_avail() == 0) { + uv_run(this->Stream->loop, UV_RUN_ONCE); + } + if (this->in_avail() == -1) { + return Traits::eof(); + } + return Traits::to_int_type(*this->gptr()); +} + +template +std::streamsize cmBasicUVStreambuf::showmanyc() +{ + if (!this->is_open() || this->EndOfFile) { + return -1; + } + return 0; +} + +template +void cmBasicUVStreambuf::StreamReadStartStop() +{ + if (this->Stream) { + uv_read_stop(this->Stream); + if (this->gptr() >= this->egptr()) { + uv_read_start( + this->Stream, + [](uv_handle_t* handle, size_t /* unused */, uv_buf_t* buf) { + auto streambuf = + static_cast*>(handle->data); + streambuf->HandleAlloc(buf); + }, + [](uv_stream_t* stream2, ssize_t nread, const uv_buf_t* /* unused */) { + auto streambuf = + static_cast*>(stream2->data); + streambuf->StreamRead(nread); + }); + } + } +} + +template +void cmBasicUVStreambuf::HandleAlloc(uv_buf_t* buf) +{ + auto size = this->egptr() - this->gptr(); + std::memmove(this->InputBuffer.data(), this->gptr(), + this->egptr() - this->gptr()); + this->setg(this->InputBuffer.data(), this->InputBuffer.data(), + this->InputBuffer.data() + size); + buf->base = this->egptr(); +#ifdef _WIN32 +# define BUF_LEN_TYPE ULONG +#else +# define BUF_LEN_TYPE size_t +#endif + buf->len = BUF_LEN_TYPE( + (this->InputBuffer.data() + this->InputBuffer.size() - this->egptr()) * + sizeof(CharT)); +#undef BUF_LEN_TYPE +} + +template +void cmBasicUVStreambuf::StreamRead(ssize_t nread) +{ + if (nread > 0) { + this->setg(this->eback(), this->gptr(), + this->egptr() + nread / sizeof(CharT)); + uv_read_stop(this->Stream); + } else if (nread < 0 || nread == UV_EOF) { + this->EndOfFile = true; + uv_read_stop(this->Stream); + } +} + +using cmUVStreambuf = cmBasicUVStreambuf; + +#endif diff --git a/Tests/CMakeLib/CMakeLists.txt b/Tests/CMakeLib/CMakeLists.txt index 031ab01..e04bba2 100644 --- a/Tests/CMakeLib/CMakeLists.txt +++ b/Tests/CMakeLib/CMakeLists.txt @@ -16,9 +16,11 @@ set(CMakeLib_TESTS testXMLSafe.cxx testFindPackageCommand.cxx testUVRAII.cxx + testUVStreambuf.cxx ) set(testRST_ARGS ${CMAKE_CURRENT_SOURCE_DIR}) +set(testUVStreambuf_ARGS $) if(WIN32) list(APPEND CMakeLib_TESTS @@ -43,7 +45,7 @@ target_link_libraries(testEncoding cmsys) foreach(testfile ${CMakeLib_TESTS}) get_filename_component(test "${testfile}" NAME_WE) - add_test(CMakeLib.${test} CMakeLibTests ${test} ${${test}_ARGS}) + add_test(NAME CMakeLib.${test} COMMAND CMakeLibTests ${test} ${${test}_ARGS}) endforeach() if(TEST_CompileCommandOutput) diff --git a/Tests/CMakeLib/testUVStreambuf.cxx b/Tests/CMakeLib/testUVStreambuf.cxx new file mode 100644 index 0000000..39655f3 --- /dev/null +++ b/Tests/CMakeLib/testUVStreambuf.cxx @@ -0,0 +1,457 @@ +#include "cmUVStreambuf.h" + +#include "cmGetPipes.h" +#include "cmUVHandlePtr.h" + +#include "cm_uv.h" + +#include +#include +#include + +#include + +#include + +#define TEST_STR_LINE_1 "This string must be exactly 128 characters long so" +#define TEST_STR_LINE_2 "that we can test CMake's std::streambuf integration" +#define TEST_STR_LINE_3 "with libuv's uv_stream_t." +#define TEST_STR TEST_STR_LINE_1 "\n" TEST_STR_LINE_2 "\n" TEST_STR_LINE_3 + +bool writeDataToStreamPipe(uv_loop_t& loop, cm::uv_pipe_ptr& inputPipe, + char* outputData, unsigned int outputDataLength, + const char* /* unused */) +{ + int err; + + // Create the pipe + int pipeHandles[2]; + if (cmGetPipes(pipeHandles) < 0) { + std::cout << "Could not open pipe" << std::endl; + return false; + } + + cm::uv_pipe_ptr outputPipe; + inputPipe.init(loop, 0); + outputPipe.init(loop, 0); + uv_pipe_open(inputPipe, pipeHandles[0]); + uv_pipe_open(outputPipe, pipeHandles[1]); + + // Write data for reading + uv_write_t writeReq; + struct WriteCallbackData + { + bool Finished = false; + int Status; + } writeData; + writeReq.data = &writeData; + uv_buf_t outputBuf; + outputBuf.base = outputData; + outputBuf.len = outputDataLength; + if ((err = uv_write(&writeReq, outputPipe, &outputBuf, 1, + [](uv_write_t* req, int status) { + auto data = static_cast(req->data); + data->Finished = true; + data->Status = status; + })) < 0) { + std::cout << "Could not write to pipe: " << uv_strerror(err) << std::endl; + return false; + } + while (!writeData.Finished) { + uv_run(&loop, UV_RUN_ONCE); + } + if (writeData.Status < 0) { + std::cout << "Status is " << uv_strerror(writeData.Status) + << ", should be 0" << std::endl; + return false; + } + + return true; +} + +bool writeDataToStreamProcess(uv_loop_t& loop, cm::uv_pipe_ptr& inputPipe, + char* outputData, unsigned int /* unused */, + const char* cmakeCommand) +{ + int err; + + inputPipe.init(loop, 0); + std::vector arguments = { cmakeCommand, "-E", "echo_append", + outputData }; + std::vector processArgs; + for (auto const& arg : arguments) { + processArgs.push_back(arg.c_str()); + } + processArgs.push_back(nullptr); + std::vector stdio(3); + stdio[0].flags = UV_IGNORE; + stdio[0].data.stream = nullptr; + stdio[1].flags = + static_cast(UV_CREATE_PIPE | UV_WRITABLE_PIPE); + stdio[1].data.stream = inputPipe; + stdio[2].flags = UV_IGNORE; + stdio[2].data.stream = nullptr; + + struct ProcessExitData + { + bool Finished = false; + int64_t ExitStatus; + int TermSignal; + } exitData; + cm::uv_process_ptr process; + auto options = uv_process_options_t(); + options.file = cmakeCommand; + options.args = const_cast(processArgs.data()); + options.flags = UV_PROCESS_WINDOWS_HIDE; + options.stdio = stdio.data(); + options.stdio_count = static_cast(stdio.size()); + options.exit_cb = [](uv_process_t* handle, int64_t exitStatus, + int termSignal) { + auto data = static_cast(handle->data); + data->Finished = true; + data->ExitStatus = exitStatus; + data->TermSignal = termSignal; + }; + if ((err = process.spawn(loop, options, &exitData)) < 0) { + std::cout << "Could not spawn process: " << uv_strerror(err) << std::endl; + return false; + } + while (!exitData.Finished) { + uv_run(&loop, UV_RUN_ONCE); + } + if (exitData.ExitStatus != 0) { + std::cout << "Process exit status is " << exitData.ExitStatus + << ", should be 0" << std::endl; + return false; + } + if (exitData.TermSignal != 0) { + std::cout << "Process term signal is " << exitData.TermSignal + << ", should be 0" << std::endl; + return false; + } + + return true; +} + +bool testUVStreambufRead( + bool (*cb)(uv_loop_t& loop, cm::uv_pipe_ptr& inputPipe, char* outputData, + unsigned int outputDataLength, const char* cmakeCommand), + const char* cmakeCommand) +{ + char outputData[] = TEST_STR; + bool success = false; + cm::uv_loop_ptr loop; + loop.init(); + cm::uv_pipe_ptr inputPipe; + std::vector inputData(128); + std::streamsize readLen; + std::string line; + cm::uv_timer_ptr timer; + + // Create the streambuf + cmUVStreambuf inputBuf(64); + std::istream inputStream(&inputBuf); + if (inputBuf.is_open()) { + std::cout << "is_open() is true, should be false" << std::endl; + goto end; + } + if ((readLen = inputBuf.in_avail()) != -1) { + std::cout << "in_avail() returned " << readLen << ", should be -1" + << std::endl; + goto end; + } + if ((readLen = inputBuf.sgetn(inputData.data(), 128)) != 0) { + std::cout << "sgetn() returned " << readLen << ", should be 0" + << std::endl; + goto end; + } + + // Perform first read test - read all the data + if (!cb(*loop, inputPipe, outputData, 128, cmakeCommand)) { + goto end; + } + inputBuf.open(inputPipe); + if (!inputBuf.is_open()) { + std::cout << "is_open() is false, should be true" << std::endl; + goto end; + } + if ((readLen = inputBuf.in_avail()) != 0) { + std::cout << "in_avail() returned " << readLen << ", should be 0" + << std::endl; + goto end; + } + if ((readLen = inputBuf.sgetn(inputData.data(), 128)) != 128) { + std::cout << "sgetn() returned " << readLen << ", should be 128" + << std::endl; + goto end; + } + if ((readLen = inputBuf.in_avail()) != 0) { + std::cout << "in_avail() returned " << readLen << ", should be 0" + << std::endl; + goto end; + } + if (std::memcmp(inputData.data(), outputData, 128)) { + std::cout << "Read data does not match write data" << std::endl; + goto end; + } + if ((readLen = inputBuf.sgetn(inputData.data(), 128)) != 0) { + std::cout << "sgetn() returned " << readLen << ", should be 0" + << std::endl; + goto end; + } + if ((readLen = inputBuf.in_avail()) != -1) { + std::cout << "in_avail() returned " << readLen << ", should be -1" + << std::endl; + goto end; + } + inputData.assign(128, char{}); + inputBuf.close(); + if (inputBuf.is_open()) { + std::cout << "is_open() is true, should be false" << std::endl; + goto end; + } + if ((readLen = inputBuf.sgetn(inputData.data(), 128)) != 0) { + std::cout << "sgetn() returned " << readLen << ", should be 0" + << std::endl; + goto end; + } + if ((readLen = inputBuf.in_avail()) != -1) { + std::cout << "in_avail() returned " << readLen << ", should be -1" + << std::endl; + goto end; + } + + // Perform second read test - read some data and then close + if (!cb(*loop, inputPipe, outputData, 128, cmakeCommand)) { + goto end; + } + inputBuf.open(inputPipe); + if (!inputBuf.is_open()) { + std::cout << "is_open() is false, should be true" << std::endl; + goto end; + } + if ((readLen = inputBuf.in_avail()) != 0) { + std::cout << "in_avail() returned " << readLen << ", should be 0" + << std::endl; + goto end; + } + if ((readLen = inputBuf.sgetn(inputData.data(), 64)) != 64) { + std::cout << "sgetn() returned " << readLen << ", should be 64" + << std::endl; + goto end; + } + if (std::memcmp(inputData.data(), outputData, 64)) { + std::cout << "Read data does not match write data" << std::endl; + goto end; + } + if ((readLen = inputBuf.in_avail()) != 8) { + std::cout << "in_avail() returned " << readLen << ", should be 8" + << std::endl; + goto end; + } + inputData.assign(128, char{}); + inputBuf.close(); + if (inputBuf.is_open()) { + std::cout << "is_open() is true, should be false" << std::endl; + goto end; + } + if ((readLen = inputBuf.in_avail()) != -1) { + std::cout << "in_avail() returned " << readLen << ", should be -1" + << std::endl; + goto end; + } + if ((readLen = inputBuf.sgetn(inputData.data(), 128)) != 0) { + std::cout << "sgetn() returned " << readLen << ", should be 0" + << std::endl; + goto end; + } + + // Perform third read test - read line by line + if (!cb(*loop, inputPipe, outputData, 128, cmakeCommand)) { + goto end; + } + inputBuf.open(inputPipe); + if (!inputBuf.is_open()) { + std::cout << "is_open() is false, should be true" << std::endl; + goto end; + } + if ((readLen = inputBuf.in_avail()) != 0) { + std::cout << "in_avail() returned " << readLen << ", should be 0" + << std::endl; + goto end; + } + + if (!std::getline(inputStream, line)) { + std::cout << "getline returned false, should be true" << std::endl; + goto end; + } + if (line != TEST_STR_LINE_1) { + std::cout << "Line 1 is \"" << line + << "\", should be \"" TEST_STR_LINE_1 "\"" << std::endl; + goto end; + } + + if (!std::getline(inputStream, line)) { + std::cout << "getline returned false, should be true" << std::endl; + goto end; + } + if (line != TEST_STR_LINE_2) { + std::cout << "Line 2 is \"" << line + << "\", should be \"" TEST_STR_LINE_2 "\"" << std::endl; + goto end; + } + + if (!std::getline(inputStream, line)) { + std::cout << "getline returned false, should be true" << std::endl; + goto end; + } + if (line != TEST_STR_LINE_3) { + std::cout << "Line 3 is \"" << line + << "\", should be \"" TEST_STR_LINE_3 "\"" << std::endl; + goto end; + } + + if (std::getline(inputStream, line)) { + std::cout << "getline returned true, should be false" << std::endl; + goto end; + } + + inputBuf.close(); + if (inputBuf.is_open()) { + std::cout << "is_open() is true, should be false" << std::endl; + goto end; + } + if ((readLen = inputBuf.in_avail()) != -1) { + std::cout << "in_avail() returned " << readLen << ", should be -1" + << std::endl; + goto end; + } + if ((readLen = inputBuf.sgetn(inputData.data(), 128)) != 0) { + std::cout << "sgetn() returned " << readLen << ", should be 0" + << std::endl; + goto end; + } + + // Perform fourth read test - run the event loop outside of underflow() + if (!cb(*loop, inputPipe, outputData, 128, cmakeCommand)) { + goto end; + } + inputBuf.open(inputPipe); + if (!inputBuf.is_open()) { + std::cout << "is_open() is false, should be true" << std::endl; + goto end; + } + if ((readLen = inputBuf.in_avail()) != 0) { + std::cout << "in_avail() returned " << readLen << ", should be 0" + << std::endl; + goto end; + } + uv_run(loop, UV_RUN_DEFAULT); + if ((readLen = inputBuf.in_avail()) != 72) { + std::cout << "in_avail() returned " << readLen << ", should be 72" + << std::endl; + goto end; + } + if ((readLen = inputBuf.sgetn(inputData.data(), 128)) != 128) { + std::cout << "sgetn() returned " << readLen << ", should be 128" + << std::endl; + goto end; + } + if ((readLen = inputBuf.in_avail()) != 0) { + std::cout << "in_avail() returned " << readLen << ", should be 0" + << std::endl; + goto end; + } + if ((readLen = inputBuf.sgetn(inputData.data(), 128)) != 0) { + std::cout << "sgetn() returned " << readLen << ", should be 128" + << std::endl; + goto end; + } + if ((readLen = inputBuf.in_avail()) != -1) { + std::cout << "in_avail() returned " << readLen << ", should be -1" + << std::endl; + goto end; + } + + inputBuf.close(); + if (inputBuf.is_open()) { + std::cout << "is_open() is true, should be false" << std::endl; + goto end; + } + if ((readLen = inputBuf.in_avail()) != -1) { + std::cout << "in_avail() returned " << readLen << ", should be -1" + << std::endl; + goto end; + } + if ((readLen = inputBuf.sgetn(inputData.data(), 128)) != 0) { + std::cout << "sgetn() returned " << readLen << ", should be 0" + << std::endl; + goto end; + } + + // Perform fifth read test - close the streambuf in the middle of a read + timer.init(*loop, &inputBuf); + if (!cb(*loop, inputPipe, outputData, 128, cmakeCommand)) { + goto end; + } + inputBuf.open(inputPipe); + if (!inputBuf.is_open()) { + std::cout << "is_open() is false, should be true" << std::endl; + goto end; + } + if ((readLen = inputBuf.in_avail()) != 0) { + std::cout << "in_avail() returned " << readLen << ", should be 0" + << std::endl; + goto end; + } + uv_timer_start(timer, + [](uv_timer_t* handle) { + auto buf = static_cast(handle->data); + buf->close(); + }, + 0, 0); + if ((readLen = inputBuf.sgetn(inputData.data(), 128)) != 0) { + std::cout << "sgetn() returned " << readLen << ", should be 0" + << std::endl; + goto end; + } + if (inputBuf.is_open()) { + std::cout << "is_open() is true, should be false" << std::endl; + goto end; + } + if ((readLen = inputBuf.in_avail()) != -1) { + std::cout << "in_avail() returned " << readLen << ", should be -1" + << std::endl; + goto end; + } + if ((readLen = inputBuf.sgetn(inputData.data(), 128)) != 0) { + std::cout << "sgetn() returned " << readLen << ", should be 0" + << std::endl; + goto end; + } + + success = true; + +end: + return success; +} + +int testUVStreambuf(int argc, char** const argv) +{ + if (argc < 2) { + std::cout << "Invalid arguments.\n"; + return -1; + } + + if (!testUVStreambufRead(writeDataToStreamPipe, argv[1])) { + std::cout << "While executing testUVStreambufRead() with pipe.\n"; + return -1; + } + + if (!testUVStreambufRead(writeDataToStreamProcess, argv[1])) { + std::cout << "While executing testUVStreambufRead() with process.\n"; + return -1; + } + + return 0; +} -- cgit v0.12