For a project I’m working on, I needed to redirect the standard output and standard error of a subprocess I’m launching. While it’s trivial to do this with NSTask (just send it setStandardOutput: and setStandardError:), I had some unique requirements that precluded this. Mainly, I didn’t want the process’ stdout/stderr redirected for its entire lifespan. Rather, I wanted it to redirect only certain parts, and return that data to the GUI through a separate mechanism (a named pipe, actually).
So, what I wanted was to be able to was:
1. Enable/disable the redirection at any point, while the process is running (this more or less precludes freopen- based solutions, as there’s no portable way to restore the streams once they’re reopened).
2. Be able to retrieve and clear the redirected output at any time, regardless of whether redirection is enabled or not.
3. stdout and stderr must both be redirected to the same buffer, and properly interleaved.
4. The helper app (the app that has this code) is written in C++; no Cocoa, no Objective-C.
Based on those requirements, I wrote the following unit test to determine what my interface should look like.
int RedirectorTest() { IOutputRedirector theRedirector; std::cout << "This is not redirected" << std::endl; theRedirector.StartRedirecting(); std::cout << "This is redirected"; assert( theRedirector.GetOutput() == "This is redirected" && "cout not redirected!" ); theRedirector.ClearOutput(); assert( theRedirector.GetOutput().length() == 0 && "redirector not cleared" ); std::cerr << "Redirected Error"; assert( theRedirector.GetOutput() == "Redirected Error" && "cerr not redirected" ); theRedirector.ClearOutput(); assert( theRedirector.GetOutput().length() == 0 && "redirector not cleared" ); theRedirector.StopRedirecting(); std::cout << "This is also not redirected"; assert( theRedirector.GetOutput().length() == 0 && "redirection not stopped!" ); theRedirector.StartRedirecting(); std::cout << "cout"; std::cerr << "cerr"; std::cout << "cout again"; std::string output = theRedirector.GetOutput(); assert( theRedirector.GetOutput() == "coutcerrcout again" && "cout, err not interleaved properly" ); return 0; }Based on that test I wrote the following interface I’d use for my test class:
class IOutputRedirector { public: IOutputRedirector() {} virtual ~IOutputRedirector(){} virtual void StartRedirecting() = 0; virtual void StopRedirecting() = 0; virtual std::string GetOutput() = 0; virtual void ClearOutput() = 0; };I Googled around for various techniques on how to do this, and the most promising one involved calling rdbuf on the cout and cerr streams. In this way, you can specify a different streambuf, and I use that of an ostringstream. It looks a little somethin’ like this:
class CCoutRedirector : public IOutputRedirector { public: CCoutRedirector(); virtual ~CCoutRedirector(); virtual void StartRedirecting(); virtual void StopRedirecting(); virtual std::string GetOutput(); virtual void ClearOutput(); private: std::streambuf* m_pOldCoutStreamBuf; std::streambuf* m_pOldCerrStreamBuf; std::ostringstream m_stream; }; CCoutRedirector::CCoutRedirector() : IOutputRedirector() , m_pOldCoutStreamBuf( NULL ) , m_pOldCerrStreamBuf( NULL ) { } CCoutRedirector::~CCoutRedirector() { StopRedirecting(); } void CCoutRedirector::StartRedirecting() { if( m_pOldCoutStreamBuf != NULL || m_pOldCerrStreamBuf != NULL ) { return; } m_pOldCoutStreamBuf = std::cout.rdbuf(); m_pOldCerrStreamBuf = std::cerr.rdbuf(); streambuf* newOutbuf = m_stream.rdbuf(); cout.rdbuf( newOutbuf ); cerr.rdbuf( newOutbuf ); } void CCoutRedirector::StopRedirecting() { if( m_pOldCoutStreamBuf != NULL ) { cout.rdbuf( m_pOldCoutStreamBuf ); m_pOldCoutStreamBuf = NULL; } if( m_pOldCerrStreamBuf != NULL ) { cerr.rdbuf( m_pOldCerrStreamBuf ); m_pOldCerrStreamBuf = NULL; } } string CCoutRedirector::GetOutput() { return m_stream.str(); } void CCoutRedirector::ClearOutput() { m_stream.str(""); }
It’s pretty self explanatory. StartRedirecting saves off the old cout and cerr’s streambufs, and replaces them with that of our own ostringstream. GetOutput returns the string from that stream; ClearOutput clears it, and finally, StopRedirecting restores the previous streambufs. Easy as pi.
With the test I wrote above this works flawlessly. There’s only one major issue, as I later discovered: while this works for cout and cerr, it does NOT work for stdout and stderr in general. Meaning, calls to printf will not be redirected. I’d not realized that stdout and cout weren’t synonymous. For me this was a deal-breaker, but perhaps others will find it useful.
In the next post I’ll show the technique I ended up using, that works for stdout/stderr as well as cout/cerr.
No comments:
Post a Comment