Commit c74698cb authored by Kyle Edwards's avatar Kyle Edwards
Browse files

cmUVStreambuf: Add std::streambuf implementation for uv_stream_t

This will allow std::istream/std::ostream-based interaction with
processes spawned by libuv.
parent 8cfd25db
......@@ -388,6 +388,7 @@ set(SRCS
cmUuid.cxx
cmUVHandlePtr.cxx
cmUVHandlePtr.h
cmUVStreambuf.h
cmUVSignalHackRAII.h
cmVariableWatch.cxx
cmVariableWatch.h
......
/* 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 <algorithm>
#include <cstring>
#include <streambuf>
#include <vector>
/*
* 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 <typename CharT, typename Traits = std::char_traits<CharT>>
class cmBasicUVStreambuf : public std::basic_streambuf<CharT, Traits>
{
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<CharT> InputBuffer;
bool EndOfFile;
void StreamReadStartStop();
void StreamRead(ssize_t nread);
void HandleAlloc(uv_buf_t* buf);
};
template <typename CharT, typename Traits>
cmBasicUVStreambuf<CharT, Traits>::cmBasicUVStreambuf(std::size_t bufSize,
std::size_t putBack)
: PutBack(std::max<std::size_t>(putBack, 1))
, InputBuffer(std::max<std::size_t>(this->PutBack, bufSize) + this->PutBack)
{
this->close();
}
template <typename CharT, typename Traits>
cmBasicUVStreambuf<CharT, Traits>::~cmBasicUVStreambuf()
{
this->close();
}
template <typename CharT, typename Traits>
bool cmBasicUVStreambuf<CharT, Traits>::is_open() const
{
return this->Stream != nullptr;
}
template <typename CharT, typename Traits>
cmBasicUVStreambuf<CharT, Traits>* cmBasicUVStreambuf<CharT, Traits>::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 <typename CharT, typename Traits>
cmBasicUVStreambuf<CharT, Traits>* cmBasicUVStreambuf<CharT, Traits>::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 CharT, typename Traits>
typename cmBasicUVStreambuf<CharT, Traits>::int_type
cmBasicUVStreambuf<CharT, Traits>::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 <typename CharT, typename Traits>
std::streamsize cmBasicUVStreambuf<CharT, Traits>::showmanyc()
{
if (!this->is_open() || this->EndOfFile) {
return -1;
}
return 0;
}
template <typename CharT, typename Traits>
void cmBasicUVStreambuf<CharT, Traits>::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<cmBasicUVStreambuf<CharT, Traits>*>(handle->data);
streambuf->HandleAlloc(buf);
},
[](uv_stream_t* stream2, ssize_t nread, const uv_buf_t* /* unused */) {
auto streambuf =
static_cast<cmBasicUVStreambuf<CharT, Traits>*>(stream2->data);
streambuf->StreamRead(nread);
});
}
}
}
template <typename CharT, typename Traits>
void cmBasicUVStreambuf<CharT, Traits>::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 <typename CharT, typename Traits>
void cmBasicUVStreambuf<CharT, Traits>::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<char>;
#endif
......@@ -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 $<TARGET_FILE:cmake>)
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)
......
#include "cmUVStreambuf.h"
#include "cmGetPipes.h"
#include "cmUVHandlePtr.h"
#include "cm_uv.h"
#include <iostream>
#include <string>
#include <vector>
#include <cstring>
#include <stdint.h>
#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<WriteCallbackData*>(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<std::string> arguments = { cmakeCommand, "-E", "echo_append",
outputData };
std::vector<const char*> processArgs;
for (auto const& arg : arguments) {
processArgs.push_back(arg.c_str());
}
processArgs.push_back(nullptr);
std::vector<uv_stdio_container_t> stdio(3);
stdio[0].flags = UV_IGNORE;
stdio[0].data.stream = nullptr;
stdio[1].flags =
static_cast<uv_stdio_flags>(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<char**>(processArgs.data());
options.flags = UV_PROCESS_WINDOWS_HIDE;
options.stdio = stdio.data();
options.stdio_count = static_cast<int>(stdio.size());
options.exit_cb = [](uv_process_t* handle, int64_t exitStatus,
int termSignal) {
auto data = static_cast<ProcessExitData*>(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<char> 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;
}