Skip to content

Commit ac345f8

Browse files
author
davidmatson
committed
Use anonymous pipes for stdout/stderr redirection.
Closes #2444. Avoid the overhead of creating and deleting temporary files. Anonymous pipes have a limited buffer and require an active reader to ensure the writer does not become blocked. Use a separate thread to ensure the buffer does not get stuck full.
1 parent 5a1ef7e commit ac345f8

File tree

3 files changed

+248
-91
lines changed

3 files changed

+248
-91
lines changed

src/catch2/internal/catch_compiler_capabilities.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@
3737
# define CATCH_CPP17_OR_GREATER
3838
# endif
3939

40+
# if (__cplusplus >= 202002L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L)
41+
# define CATCH_CPP20_OR_GREATER
42+
# endif
43+
4044
#endif
4145

4246
// Only GCC compiler should be used in this block, so other compilers trying to

src/catch2/internal/catch_output_redirect.cpp

Lines changed: 175 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@
1414
#include <sstream>
1515

1616
#if defined(CATCH_CONFIG_NEW_CAPTURE)
17+
#include <system_error>
1718
#if defined(_MSC_VER)
18-
#include <io.h> //_dup and _dup2
19+
#include <fcntl.h> // _O_TEXT
20+
#include <io.h> // _close, _dup, _dup2, _fileno, _pipe and _read
21+
#define close _close
1922
#define dup _dup
2023
#define dup2 _dup2
2124
#define fileno _fileno
2225
#else
23-
#include <unistd.h> // dup and dup2
26+
#include <unistd.h> // close, dup, dup2, fileno, pipe and read
2427
#endif
2528
#endif
2629

@@ -60,85 +63,202 @@ namespace Catch {
6063

6164
#if defined(CATCH_CONFIG_NEW_CAPTURE)
6265

63-
#if defined(_MSC_VER)
64-
TempFile::TempFile() {
65-
if (tmpnam_s(m_buffer)) {
66-
CATCH_RUNTIME_ERROR("Could not get a temp filename");
67-
}
68-
if (fopen_s(&m_file, m_buffer, "w+")) {
69-
char buffer[100];
70-
if (strerror_s(buffer, errno)) {
71-
CATCH_RUNTIME_ERROR("Could not translate errno to a string");
72-
}
73-
CATCH_RUNTIME_ERROR("Could not open the temp file: '" << m_buffer << "' because: " << buffer);
74-
}
66+
inline void close_or_throw(int descriptor)
67+
{
68+
if (close(descriptor))
69+
{
70+
throw std::system_error{ errno, std::generic_category() };
7571
}
76-
#else
77-
TempFile::TempFile() {
78-
m_file = std::tmpfile();
79-
if (!m_file) {
80-
CATCH_RUNTIME_ERROR("Could not create a temp file.");
81-
}
72+
}
73+
74+
inline int dup_or_throw(int descriptor)
75+
{
76+
int result{ dup(descriptor) };
77+
78+
if (result == -1)
79+
{
80+
throw std::system_error{ errno, std::generic_category() };
81+
}
82+
83+
return result;
84+
}
85+
86+
inline int dup2_or_throw(int sourceDescriptor, int destinationDescriptor)
87+
{
88+
int result{ dup2(sourceDescriptor, destinationDescriptor) };
89+
90+
if (result == -1)
91+
{
92+
throw std::system_error{ errno, std::generic_category() };
8293
}
8394

95+
return result;
96+
}
97+
98+
inline int fileno_or_throw(std::FILE* file)
99+
{
100+
int result{ fileno(file) };
101+
102+
if (result == -1)
103+
{
104+
throw std::system_error{ errno, std::generic_category() };
105+
}
106+
107+
return result;
108+
}
109+
110+
inline void pipe_or_throw(int descriptors[2])
111+
{
112+
#if defined(_MSC_VER)
113+
constexpr int defaultPipeSize{ 0 };
114+
115+
int result{ _pipe(descriptors, defaultPipeSize, _O_TEXT) };
116+
#else
117+
int result{ pipe(descriptors) };
84118
#endif
85119

86-
TempFile::~TempFile() {
87-
// TBD: What to do about errors here?
88-
std::fclose(m_file);
89-
// We manually create the file on Windows only, on Linux
90-
// it will be autodeleted
120+
if (result)
121+
{
122+
throw std::system_error{ errno, std::generic_category() };
123+
}
124+
}
125+
126+
inline size_t read_or_throw(int descriptor, void* buffer, size_t size)
127+
{
91128
#if defined(_MSC_VER)
92-
std::remove(m_buffer);
129+
int result{ _read(descriptor, buffer, static_cast<unsigned>(size)) };
130+
#else
131+
ssize_t result{ read(descriptor, buffer, size) };
93132
#endif
133+
134+
if (result == -1)
135+
{
136+
throw std::system_error{ errno, std::generic_category() };
94137
}
95138

139+
return static_cast<size_t>(result);
140+
}
96141

97-
FILE* TempFile::getFile() {
98-
return m_file;
142+
inline void fflush_or_throw(std::FILE* file)
143+
{
144+
if (std::fflush(file))
145+
{
146+
throw std::system_error{ errno, std::generic_category() };
99147
}
148+
}
100149

101-
std::string TempFile::getContents() {
102-
std::stringstream sstr;
103-
char buffer[100] = {};
104-
std::rewind(m_file);
105-
while (std::fgets(buffer, sizeof(buffer), m_file)) {
106-
sstr << buffer;
107-
}
108-
return sstr.str();
150+
jthread::jthread() noexcept : m_thread{} {}
151+
152+
template <typename F, typename... Args>
153+
jthread::jthread(F&& f, Args&&... args) : m_thread{ std::forward<F>(f), std::forward<Args>(args)... } {}
154+
155+
// Not exactly like std::jthread, but close enough for the code below.
156+
jthread::~jthread() noexcept
157+
{
158+
if (m_thread.joinable())
159+
{
160+
m_thread.join();
109161
}
162+
}
163+
164+
constexpr UniqueFileDescriptor::UniqueFileDescriptor() noexcept : m_value{} {}
110165

111-
OutputRedirect::OutputRedirect(std::string& stdout_dest, std::string& stderr_dest) :
112-
m_originalStdout(dup(1)),
113-
m_originalStderr(dup(2)),
114-
m_stdoutDest(stdout_dest),
115-
m_stderrDest(stderr_dest) {
116-
dup2(fileno(m_stdoutFile.getFile()), 1);
117-
dup2(fileno(m_stderrFile.getFile()), 2);
166+
UniqueFileDescriptor::UniqueFileDescriptor(int value) noexcept : m_value{ value } {}
167+
168+
constexpr UniqueFileDescriptor::UniqueFileDescriptor(UniqueFileDescriptor&& other) noexcept :
169+
m_value{ other.m_value }
170+
{
171+
other.m_value = 0;
172+
}
173+
174+
UniqueFileDescriptor::~UniqueFileDescriptor() noexcept
175+
{
176+
if (m_value == 0)
177+
{
178+
return;
118179
}
119180

120-
OutputRedirect::~OutputRedirect() {
121-
Catch::cout() << std::flush;
122-
fflush(stdout);
123-
// Since we support overriding these streams, we flush cerr
124-
// even though std::cerr is unbuffered
125-
Catch::cerr() << std::flush;
126-
Catch::clog() << std::flush;
127-
fflush(stderr);
181+
close_or_throw(m_value); // std::terminate on failure (due to noexcept)
182+
}
128183

129-
dup2(m_originalStdout, 1);
130-
dup2(m_originalStderr, 2);
184+
UniqueFileDescriptor& UniqueFileDescriptor::operator=(UniqueFileDescriptor&& other) noexcept
185+
{
186+
if (this != &other)
187+
{
188+
if (m_value != 0)
189+
{
190+
close_or_throw(m_value); // std::terminate on failure (due to noexcept)
191+
}
131192

132-
m_stdoutDest += m_stdoutFile.getContents();
133-
m_stderrDest += m_stderrFile.getContents();
193+
m_value = other.m_value;
194+
other.m_value = 0;
134195
}
135196

197+
return *this;
198+
}
199+
200+
constexpr int UniqueFileDescriptor::get() { return m_value; }
201+
202+
inline void create_pipe(UniqueFileDescriptor& readDescriptor, UniqueFileDescriptor& writeDescriptor)
203+
{
204+
readDescriptor = {};
205+
writeDescriptor = {};
206+
207+
int descriptors[2];
208+
pipe_or_throw(descriptors);
209+
210+
readDescriptor = UniqueFileDescriptor{ descriptors[0] };
211+
writeDescriptor = UniqueFileDescriptor{ descriptors[1] };
212+
}
213+
214+
inline void read_thread(UniqueFileDescriptor&& file, std::string& result)
215+
{
216+
std::string buffer{};
217+
constexpr size_t bufferSize{ 4096 };
218+
buffer.resize(bufferSize);
219+
size_t sizeRead{};
220+
221+
while ((sizeRead = read_or_throw(file.get(), &buffer[0], bufferSize)) != 0)
222+
{
223+
result.append(buffer.data(), sizeRead);
224+
}
225+
}
226+
227+
OutputFileRedirector::OutputFileRedirector(FILE* file, std::string& result) :
228+
m_file{ file },
229+
m_fd{ fileno_or_throw(m_file) },
230+
m_previous{ dup_or_throw(m_fd) }
231+
{
232+
fflush_or_throw(m_file);
233+
234+
UniqueFileDescriptor readDescriptor{};
235+
UniqueFileDescriptor writeDescriptor{};
236+
create_pipe(readDescriptor, writeDescriptor);
237+
238+
// Anonymous pipes have a limited buffer and require an active reader to ensure the writer does not become blocked.
239+
// Use a separate thread to ensure the buffer does not get stuck full.
240+
m_readThread = jthread{ [readDescriptor{ std::move(readDescriptor) }, &result] () mutable {
241+
read_thread(std::move(readDescriptor), result); } };
242+
243+
dup2_or_throw(writeDescriptor.get(), m_fd);
244+
}
245+
246+
OutputFileRedirector::~OutputFileRedirector() noexcept
247+
{
248+
fflush_or_throw(m_file); // std::terminate on failure (due to noexcept)
249+
dup2_or_throw(m_previous.get(), m_fd); // std::terminate on failure (due to noexcept)
250+
}
251+
252+
OutputRedirect::OutputRedirect(std::string& output, std::string& error) :
253+
m_output{ stdout, output }, m_error{ stderr, error } {}
254+
136255
#endif // CATCH_CONFIG_NEW_CAPTURE
137256

138257
} // namespace Catch
139258

140259
#if defined(CATCH_CONFIG_NEW_CAPTURE)
141260
#if defined(_MSC_VER)
261+
#undef close
142262
#undef dup
143263
#undef dup2
144264
#undef fileno

0 commit comments

Comments
 (0)