1+1=10

Debao's Blog...

"Redirect current process's stdout to a Widget such as QTextEdit"

Note:

  • Source code can be got from https://github.com/dbzhang800/StdoutRedirector
  • This class can only be used in Qt5, as QWindowsPipeReader which is introduced in Qt5.0 is used.

Implementation

Windows

  • Normally, we need to create pipe with CreatePipe(), then attach stdout to it's write end with SetStdHandle(), then read from pipe's read end with ReadFile().
    createWinPipe(hRead, hWrite);
    if (m_channels & StandardOutput)
        ::SetStdHandle(STD_OUTPUT_HANDLE, hWrite);
    if (m_channels & StandardError)
        ::SetStdHandle(STD_ERROR_HANDLE, hWrite);
  • But the CRT has already completed initialization before the application gets a chance to call SetStdHandle(); the three low I/O handles 0, 1, and 2 have already been set up to use the original OS handles. So we must deal with this layer using posix api _dup2() too.
    int fd = _open_osfhandle((intptr_t)hWrite, _O_WRONLY|_O_TEXT);
    if (m_channels & StandardOutput)
        _dup2(fd, 1);
    if (m_channels & StandardError)
        _dup2(fd, 2);
    _close(fd);
  • Anonymous pipes created with CreatePipe() do not support asynchronous I/O, so named pipe is used.
static void createWinPipe(HANDLE &hRead, HANDLE &hWrite)
{
    QString pipeName = QString::fromLatin1("\\\\.\\pipe\\stdoutredirector-%1").arg(QUuid::createUuid().toString());
    SECURITY_ATTRIBUTES attributes = {sizeof(SECURITY_ATTRIBUTES), 0, false};
    hRead = ::CreateNamedPipe((wchar_t*)pipeName.utf16(), PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED,
                                  PIPE_TYPE_BYTE | PIPE_WAIT, 1, 0, 1024 * 1024, 0, &attributes);

    SECURITY_ATTRIBUTES attributes2 = {sizeof(SECURITY_ATTRIBUTES), 0, true};
    hWrite = ::CreateFile((wchar_t*)pipeName.utf16(), GENERIC_WRITE,
                        0, &attributes2, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);

    ::ConnectNamedPipe(hRead, NULL);
}
  • QWindowsPipeReader is used to watch the pipe read end.
    pipeReader = new QWindowsPipeReader(this);
    pipeReader->setHandle(hRead);
    pipeReader->startAsyncRead();
    connect(pipeReader, SIGNAL(readyRead()), this, SIGNAL(readyRead()));

Posix

  • First, we create a pipe, then make the writable end of the pipe the new stdout, and finally, read from the readable part of the pipe.
    ::pipe(pipeEnds);
    if (m_channels & StandardOutput)
        ::dup2(pipeEnds[1], 1);
    if (m_channels & StandardError)
        ::dup2(pipeEnds[1], 2);
    ::close(pipeEnds[1]);
  • QSocketNotifier is used in order to monitor the activity of the pipe-read-end.
    socketNotifier = new QSocketNotifier(pipeEnds[0], QSocketNotifier::Read, this);
    connect(socketNotifier, SIGNAL(activated(int)), this, SLOT(onSocketActivated()));
  • Note that QSocketNotifier will keep emitting signal if data exists in the pipe, while our readyRead() only emit when new data arrival, so QRingBuffer is used as a buffer.

Issues

  • [Windows]Can't capture the stdout output generated by another dlls.

If the dll compiled with a c/c++ run-time which is different from the run-time used by current application, or the run-time staticly linked to the dll or application, we will encounter this problem.

The reason is that, the DLL grabs the stdout handles when it is loaded, which took place before we changed the stdout handles. Dynamically load the DLL after changing the stdout handles will be helpful in this case.

  • [Windows]Can't capture the output of qDebug()

When the application build as a GUI application(without CONFIG += console in the .pro file), the debug messages will be send to the Debuger using the system api OutputDebugString().

OutputDebugString(reinterpret_cast<const wchar_t *>(logMessage.utf16()));

If CONFIG+=console is added to the .pro file, the debug message will be sent to the stderr.

fprintf(stderr, "%s", logMessage.toLocal8Bit().constData());

Reference

Qt

Comments