- I needed to run some code in a separate process and collect data from it as structured text (as if I was calling a the program with
popen
).
- The program in question used a library that wrote notification of unexpected events to the standard output.
Of course you could argue that a well designed library wouldn’t spew trash into the a resource “owned” by the calling code like that (and you’d be right). But this is a legacy tool that lies outside my control: I have to take it as it comes and find a way to deal.
I used an approach at work that relied on details of the context and the fact that offending output was all on the standard error. But my subconscious worried the general problem until the approach discussed herein occurred to me.
Go Talk in Another Room
I’d like to simply re-direct the output of the library to some harmless place (possibly
/dev/null
or possibly a log of some kind).But I also want to be able to restore the stream assignment we had when I’m done calling the code with the issue.
And I want it to work with output generated by both
stdio.h
and iostreams
. (I could probably find out which one this offending library is using, but this might as well be a general facility.And I want it to be safe, convenient and easy.
Programming to standard
The actual C and C++ standards provide surprisingly thin and weak abstraction for files. I mean C provides an opaque type
FILE
and a set of interfaces for opening, reading, writing, seeking, and closing them, but it also provides three special FILE*
objects for your programs to use that generally can’t be set up using the open and close primitives in the API.To manipulate with
stdin
, stdout
, and stderr
we need to bring in some OS API. I’ll be programming against POSIX as expressed in unistd.h
(and I’ll try to provide support for windows systems by using some POSIX compatibility calls provided by that system).Using RAII and scope
This code to acquire a resource, wait for stuff to happen and then give it back without caring exactly what is going on in the middle step as long as the resource is in the expected state when the “give it back” code runs.
This is a suitable use case for RAII (Resource Acquisition Is Initialization). We’re going to mute the selected stream in the constructor of a class and unmute it in the destructor. Then we control when the stream is muted using the lifetime of objects of the class.
If we call the class that does this
Muffle
a typical application might look likeoperationWhoseOutputYouWantToSee();
{ // scoping brace control the object's lifetime
Muffle o(stdout);
someOperationWhoseOutputNeedsSuppressing();
}
anotherOperationYouWantToLetOutput();
This same is also seen in classes like std::lock_gaurd
and Qt’s QSignalBlocker
.RAII is not automatic
Used consistently RAII can be a bit magical, because as long as all the members of your class are fundamental type or also implement RAII you don’t have to worry about them: you construct them in your constructor and let your d’tor call theirs and they take care of themselves.
But in this case we’re interacting with a resource (OS file descriptors) that are handled through a C library. We’re going to have to pay attention to make sure that we aren’t leaking file descriptors.
The code
Written against C+±11 as that is what I’ve been using at work recently. (I know it’s getting long in the tooth but we are required to support some pretty antique platforms; with this standard we only have to construct a custom build environment for one of them…)
Header file (
muffle.h
):#ifndef MUFFLE_H
#define MUFFLE_H
// A RAII class that redirects an output stream to the a file (the bit
// bucket by default) during it's lifetime.
#include <cstdio> // For FILE*
#include "ext.h"
class Muffle
{
public:
Muffle(int fd, const std::string & fname = os::bitBucket() );
Muffle(FILE * stream, const std::string & fname = os::bitBucket() );
~Muffle();
private:
int m_origFd; /// File descriptor associated with original state; gets temporarily re-written
int m_tempFd; /// Holds the a clone of the original to restore
};
#endif//MUFFLE_H
(The function os::bitBucket
is defined in ext.h
and return "/dev/null"
or "NUL"
on Unix-alike and Windows systems respectively.)Source file (
muffle.cpp
):#include "muffle.h"
#if defined(_WIN32) || defined(_WIN64)
#include <io.h>
#define close _close
#define dup _dup
#define dup2 _dup2
#define fclose _fclose
#define fileno _fileno
#else
#include <unistd.h>
#endif
#include <stdexcept>
Muffle::Muffle(int fd, const std::string & fname)
: m_origFd(fd)
, m_tempFd(dup(m_origFd)) // +1 fd
{
FILE * bitBucket = fopen(fname.c_str(), "w+"); // +1 fd
if ( ! bitBucket )
throw std::runtime_error( std::string("Muffle: Error: "
"Failed to open '") +
fname + "' to recieve redirected output.");
// In principle we could manipulate the buffering here (setvbuf() or
// similar)
dup2( fileno(bitBucket), m_origFd ); // closes then re-opens m_origFd
fclose(bitBucket); // -1 fd
}
Muffle::Muffle(FILE * stream, const std::string & fname)
: Muffle(fileno(stream), fname)
{}
Muffle::~Muffle()
{
dup2( m_tempFd, m_origFd ); // closes then re-opens m_origFd
close(m_tempFd); // -1 fd
}
BTW, the Windows support code there is just a guess. I haven’t tested it.Seeing it work
You can see what it does with a simple scaffold:
#include "muffle.h"
// Test the blocking behavior on both c stdio and c++ iostreams, covering both
// the standard output and the standard input. Also check that everything is
// properly restored.
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <string>
// Simply attempt to output it's argument on (both standard streams) x (both
// standard library mechanisms).
void testStreams(const std::string & word)
{
fprintf(stdout,"\tstdio: stdout: '%s'\n",word.c_str());
fprintf(stderr,"\tstdio: stderr: '%s'\n",word.c_str());
std::cout << "\tstreams: cout: '" << word << "'" << std::endl;
std::cerr << "\tstreams: cerr: '" << word << "'" << std::endl;
}
int main(void)
{
std::cout << "\n" << "Check initial function ..." << "\n" << std::endl;
testStreams("Before blocking");
std::cout << "\n" << "Block stdout ..." << "\n" << std::endl;
{
Muffle o(stdout);
testStreams("stdout blocked");
}
std::cout << "\n" << "Block stderr ..." << "\n" << std::endl;
{
Muffle e(stderr);
testStreams("stderr blocked");
}
std::cout << "\n" << "Block both ..." << "\n" << std::endl;
{
Muffle o(stdout);
Muffle e(stderr);
testStreams("stdout/stderr blocked");
}
std::cout << "\n" << "Check full restoration ..." << "\n" << std::endl;
testStreams("After blocking");
return EXIT_SUCCESS;
}
On the Mac and Linux system where I’ve tested it it spits out:
Check initial function ...
stdio: stderr: 'Before blocking'
stdio: stdout: 'Before blocking'
streams: cout: 'Before blocking'
streams: cerr: 'Before blocking'
Block stdout ...
stdio: stderr: 'stdout blocked'
streams: cerr: 'stdout blocked'
Block stderr ...
stdio: stdout: 'stderr blocked'
streams: cout: 'stderr blocked'
Block both ...
Check full restoration ...
stdio: stderr: 'After blocking'
stdio: stdout: 'After blocking'
streams: cout: 'After blocking'
streams: cerr: 'After blocking'
BTW, I do know that I don’t need the flushing behavior of
std::endl
all over the place like that. Too bad. I prefer to have belt and suspenders until I know I’m IO limited to an unacceptable degree.Is it watertight?
Early versions of the code leaked file descriptors. Not good. Another simple scaffold tests for that.
#include "muffle.h"
// Attempt to detect if we're leaking file descriptors, by simply cycling the
// class a lot.
//
// Accepts a single, optional command line argument specifying how many times to
// cycle (defaults to 1025 to break default-configured Linux if we lose one per
// cycle).
#include <cstdio>
#include <iostream>
int main(int argc, char**argv)
{
size_t max = 1025;
if (argc > 1)
max = atol(argv[1]);
for (size_t i=0; i<max; ++i)
{
Muffle e(stderr);
std::cout << i << std::endl;
}
return EXIT_SUCCESS;
}
This output is boring as it simple counts up to one less than it’s argument.
Discussion
I had to take some care to follow the acquisition and relinquishment of file descriptors and have left a trail of bread-crumbs from that investigation in the form of end-of-line comments. The upshot is that the class claims a single net fd for most of its lifetime, but need to grab two briefly during the c’tor.
Deficiencies
This code is not thread safe as it relies on a LIFO structure of calls (which is enforced by the scope stack in single-threaded programming).
Written with StackEdit.
No comments:
Post a Comment