Ways to Print and Capture Text Output of a Process

Sun
02
Jul 2023

In my previous blog post “Launching process programmatically: system vs CreateProcess vs ShellExecute”, I investigated various ways of launching a new process when programming in C++ using Windows, with the focus on different ways to specify a path to the executable file. Today, I want to describe a related topic: we will investigate ways that a process can print some text messages (standard output, standard error, WriteConsole function, DebugOutputString function), how we can observe this output and, finally, how we can capture it programmatically when launching a subprocess using CreateProcess function.

Visual Studio / C++ project accompanying this article: github.com/sawickiap/TextOutputTest

Standard output

In that previous post, I’ve stated that every process has some startup parameters: executable file path, command line parameters, working directory, and environmental variables. During its lifetime, it also has access to 3 standard “streams of text” – standard input (stdin) that can be read, standard output (stdout) and standard error (stderr) that can be written. When we learn a new programming language and we write the classical first program that prints “Hello World”, this text is actually sent to the standard output. In C++, it doesn’t matter if you use the legacy C API (functions like printf) or new C++ streams (std::cout) to print your text messages – they all end up on the same standard output. It also doesn’t matter if you use single-byte (ANSI) characters or Unicode (wprintf, std::wcout). In this article I will use Unicode versions.

If the project is created as a console application, we can see these messages in the console window, printed on a black background:

#include <cstdio>
#include <iostream>

int wmain()
{
  wprintf(L"wprintf: Hello World!\n");
  std::wcout << L"std::wcout: Hello World!\n";
}

Windows versus console app

In Visual Studio project options, you can select Linker > System > SubSystem as Console (/SUBSYSTEM:CONSOLE) or Windows (/SUBSYSTEM:WINDOWS). Only the first variant makes the program opening a console. Also, first variant requires to implement function main (or wmain) as the main entry point, while the second one requires to implement function WinMain (or wWinMain) with a different signature.

This division between console and windows application is not that simple. A program configured as console can still create windows. Nothing prevents it from doing the usual WinAPI ritual with RegisterClassEx, CreateWindowEx, GetMessage, TranslateMessage, DispatchMessage etc., to show some GUI next to its console. A windows program, on the other hand, can also manually open a console window by just calling simple, 0-parameter function AllocConsole(). It can also write to its standard output using functions like shown above. There is one problem though – such messages sent to the standard output don’t show up in the console opened manually using AllocConsole. They also don’t show up on an existing console that you may use to launch the app. They are, however, written to a file if we redirect the standard output to a file using > operator in Windows shell.

#include <cstdio>
#include <iostream>
#include <Windows.h>

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR lpCmdLine, int nCmdShow)
{
 wprintf(L"wprintf: Hello World!\n");
 std::wcout << L"std::wcout: Hello World!\n";
}

Standard output versus console

This observation brings us to another topic: Console (the system window with black background) is not the same as standard output (the virtual stream of characters that each program has available for writing). Actually, WinAPI offers additional, more native and more feature-rich function to control the console directly. Function WriteConsole can be used to print messages. In applications configured as SubSystem = Console, it works the same way as methods shown above, but it also works in applications configured as SubSystem = Windows with console created manually using AllocConsole.

#include <cstdio>
#include <iostream>
#include <Windows.h>

int wmain()
{
 wprintf(L"wprintf: Hello World!\n");

 std::wcout << L"std::wcout: Hello World!\n";

 HANDLE consoleHandle = GetStdHandle(STD_OUTPUT_HANDLE);
 const WCHAR* writeConsoleMessage = L"WriteConsole: Hello World!\n";
 DWORD charsWritten = 0;
 WriteConsole(consoleHandle, writeConsoleMessage, (DWORD)wcslen(writeConsoleMessage),
 &charsWritten, NULL);

Interestingly, when this program gets redirected to a file, first 2 messages are written to the file, but the message printed using WriteConsole is lost – appears neither in the console nor in the file.

There are more functions available in WinAPI for controlling the console, including SetConsoleTextAttribute for changing text color and background color, or SetConsoleCursorPosition. With them, you could even write a game rendered entirely in console using ASCII art – which I actually did back in 2003 for a game jam :)

There is a method to fix standard input/output streams to work with manually opened console, which I described long time ago in my article "Redirecting Standard I/O to Windows Console", but it doesn’t seem to work for me now and I don’t know why.

Standard error

There is a second stream of text that every process has next to standard output, called standard error. Both are output streams, but this one has a special purpose of reporting errors. In order to write to it, we need to use fprintf or fwprintf function as if we were writing to a file, but instead of opening a real file using fopen, we specify a special predefined variable stderr as the FILE* handle. When running the program normally as a console app, all these messages are printed to the console, so there is no difference between stdout and stderr.

wprintf(L"wprintf: Hello World!\n");
std::wcout << L"std::wcout: Hello World!\n";
fwprintf(stderr, L"fwprintf to stderr: Hello World!\n");

A good use case could be a program that processes some data and prints the results to its standard output. We may want to redirect them to a file, but in case of failure, we want to see an error message on the console rather than writing it to that file.

There is also a way to redirect standard error to the same file as standard output using magic sequence “2>&1”:

Interestingly, the error message ended up first despite it was written last. Maybe standard output and standard error streams are buffered independently and standard error gets flushed immediately after each call to fwprintf?

Capturing process output

When we write a separate “host” application that launches our “test” application described earlier, question is what happens with messages printed by the “test” application. Let’s use CreateProcess function, like described in my previous article:

wprintf(L"[ Hello World from the host! ]\n");

wchar_t path[MAX_PATH];
wcscpy_s(path, L"c:/Code/TextOutputTest/x64/Debug/Test.exe");

STARTUPINFO startupInfo = {
.cb = sizeof(STARTUPINFO)
};
PROCESS_INFORMATION processInfo = {};
BOOL success = CreateProcess(
path, // lpApplicationName
NULL, // lpCommandLine
NULL, // lpProcessAttributes
NULL, // lpThreadAttributes
TRUE, // bInheritHandles
0, // dwCreationFlags
NULL, // lpEnvironment
NULL, // lpCurrentDirectory
&startupInfo,
&processInfo);
assert(success);

WaitForSingleObject(processInfo.hProcess, INFINITE);
CloseHandle(processInfo.hThread);
CloseHandle(processInfo.hProcess);

When both are console projects, there is just one console window opened and the guest app writes all the messages to the console of the host app:

Can we somehow capture the output printed by our subprocess and process it, for example print it converted to upper case letters? Yes we can, but it requires several steps. Let’s see the code first, and then I will explain everything.

SECURITY_ATTRIBUTES pipeSecurityAttributes = {
 .nLength = sizeof(SECURITY_ATTRIBUTES),
 .bInheritHandle = TRUE };
HANDLE readPipe, writePipe;
BOOL success = CreatePipe(
 &readPipe, // hReadPipe
&writePipe, // hWritePipe
 &pipeSecurityAttributes, // lpPipeAttributes
 0); // nSize (0 means default)
assert(success);

auto readingThreadFunc = [readPipe]()
{
 char buf[256];
 for(;;)
 {
 DWORD dw;
 BOOL ok = ReadFile(readPipe, buf, sizeof(buf), &dw, NULL);
 if(ok)
 {
 for(DWORD i = 0; i < dw; ++i)
printf("%c", toupper(buf[i]));
  }
 else
 return;
 }
};
std::thread readingThread{readingThreadFunc};

wchar_t path[MAX_PATH];
wcscpy_s(path, L"c:/Code/TextOutputTest/x64/Debug/Test.exe");
STARTUPINFO startupInfo = {
 .cb = sizeof(STARTUPINFO),
 .dwFlags = STARTF_USESTDHANDLES,
 .hStdInput = INVALID_HANDLE_VALUE,
 .hStdOutput = writePipe,
 .hStdError = writePipe
};
PROCESS_INFORMATION processInfo = {};
success = CreateProcess(
 path, // lpApplicationName
 NULL, // lpCommandLine
 NULL, // lpProcessAttributes
 NULL, // lpThreadAttributes
 TRUE, // bInheritHandles
 0, // dwCreationFlags
 NULL, // lpEnvironment
 NULL, // lpCurrentDirectory
 &startupInfo,
 &processInfo);
assert(success);

WaitForSingleObject(processInfo.hProcess, INFINITE);
CloseHandle(processInfo.hThread);
CloseHandle(processInfo.hProcess);

CloseHandle(writePipe);
CloseHandle(readPipe);

readingThread.join();

Starting from the end, we launch the subprocess using CreateProcess function, like before. This time, however, we setup additional parameters of the STARTUPINFO structure – we set STARTF_USESTDHANDLES flag and assign some values to hStdInput, hStdOuptut, hStdError. This way, we can specify some handles that will become standard input, output, and error streams for our subprocess.

These handles are earlier created as a an anonymous pipe using CreatePipe function. A pipe is an object from WinAPI that has a handle, like an opened file, but it is just a buffer in memory that can be written and read. There are actually two handles – writePipe intended for writing, which we pass to our subprocess to become its standard output and standard error, and readPipe intended for reading, which we use on our end to read the messages. It is also important to set SECURITY_ATTRIBUTES with bInheritHandle = TRUE when creating the pipe, so that the subprocess can use it.

When trying to read from the pipe, there is one problem: We want to do the reading as long as there is some data in the pipe, but we also want to wait until the test process exits. The way I solved it, as you can see above, is that I wait for the process on the main thread, while a separate thread is spawned to call ReadFile in a loop to read the characters written by it and print them to our read standard output, converted to upper case.

It works and doesn’t hang because when the main thread observes the end of the subprocess, it closes the handles of the pipe. Then, on the other thread, ReadFile function fails (variable ok is false), which returns from the loop and ends the worker thread, which can then be joined in the main thread. The order is important here. Alternatively, I could avoid using multithreading here, but use asynchronous (overlapped) I/O instead when calling ReadFile. However, this would require to create a named pipe not an anonymous pipe, which would complicate the code.

OutputDebugString

There is one more way of outputting text messages from a process – another WinAPI function called OutputDebugString. It has an ANSI and Unicode version, like most functions from Windows API that accept a string. It has no formatting capabilities – you need to provide a ready string for it. This one is intended for debugging. You won’t see the message on the standard output or in the console.

First way to see it is to launch the program under a debugger. When using Visual Studio and launching the app with a debugger by pressing F5, we can see the message in the Output panel, among messages generated automatically when a process or thread starts, ends, or loads a DLL:

When not running under a debugger, the hope to see this message is not completely lost. There is a 3rd party tool that can capture these messages across the entire system – DebugView (or rather DbgView64.exe) – part of the free SysIntenals tool suite from Microsoft.

Capturing OutputDebugString

Is there a way to capture messages printed using OutputDebugString? Yes, although this is not so easy. I found two ways to do this described on the Internet. First way, and probably the one that DebugView uses, is system-wide. It requires to open some mutexes, events, and shared memory with specific names: DBWinMutex, DBWIN_BUFFER, DBWIN_BUFFER_READY, DBWIN_DATA_READY, as described in the article Understanding Win32 "OutputDebugString" at Steve Friedl's Unixwiz.net Tech Tips, for example. We won’t use it here.

The second way works with subprocesses spawned from our process. It requires to write a simple debugger. It sounds scary, but the code is not that long, as you can see below. All we need to do is to pass a special flag DEBUG_PROCESS when calling CreateProcess and then to write a loop that will fetch and process “debug events” of different kinds using function WaitForDebugEventEx.

SECURITY_ATTRIBUTES pipeSecurityAttributes = {
 .nLength = sizeof(SECURITY_ATTRIBUTES),
 .bInheritHandle = TRUE };
HANDLE readPipe, writePipe;
BOOL success = CreatePipe(
 &readPipe, // hReadPipe
 &writePipe, // hWritePipe
 &pipeSecurityAttributes, // lpPipeAttributes
 0); // nSize (0 means default)
assert(success);

auto readingThreadFunc = [readPipe]()
{
 char buf[256];
 for(;;)
 {
 DWORD dw;
 BOOL ok = ReadFile(readPipe, buf, sizeof(buf), &dw, NULL);
 if(ok)
 {
 if(dw > 0)
 printf("%.*s", (int)dw, buf);
 }
 else
 return;
 }
};
std::thread readingThread{readingThreadFunc};

wchar_t path[MAX_PATH];
wcscpy_s(path, L"Test.exe");
STARTUPINFO startupInfo = {
 .cb = sizeof(STARTUPINFO),
 .dwFlags = STARTF_USESTDHANDLES,
 .hStdInput = INVALID_HANDLE_VALUE,
 .hStdOutput = writePipe,
 .hStdError = writePipe
};
PROCESS_INFORMATION processInfo = {};
success = CreateProcess(
 path, // lpApplicationName
 NULL, // lpCommandLine
 NULL, // lpProcessAttributes
 NULL, // lpThreadAttributes
 TRUE, // bInheritHandles
 DEBUG_PROCESS, // dwCreationFlags
 NULL, // lpEnvironment
 NULL, // lpCurrentDirectory
 &startupInfo,
 &processInfo);
assert(success);

DEBUG_EVENT e;
bool running = true;
while(running)
{
 ZeroMemory(&e, sizeof(e));
 BOOL ok = WaitForDebugEventEx(&e, INFINITE); // Must be on the same thread as CreateProcess!!!
 assert(ok);
 DWORD continueStatus = DBG_CONTINUE;
 switch(e.dwDebugEventCode)
 {
 case EXCEPTION_DEBUG_EVENT:
 if(e.u.Exception.ExceptionRecord.ExceptionCode != EXCEPTION_BREAKPOINT)
 continueStatus = DBG_EXCEPTION_NOT_HANDLED;
 break;
 case CREATE_PROCESS_DEBUG_EVENT:
 CloseHandle(e.u.CreateProcessInfo.hFile);
 break;
  case EXIT_PROCESS_DEBUG_EVENT:
 if(e.dwProcessId == processInfo.dwProcessId)
 running = false;
 break;
 case OUTPUT_DEBUG_STRING_EVENT:
if(e.u.DebugString.nDebugStringLength > 0)
 {
 char buf[1024];
WORD len = e.u.DebugString.nDebugStringLength;
 assert(len < _countof(buf));
 size_t bytesRead = 0;
 ok = ReadProcessMemory(processInfo.hProcess, e.u.DebugString.lpDebugStringData, buf, len, &bytesRead);
 assert(ok && bytesRead == len);
 if(e.u.DebugString.fUnicode)
 wprintf(L"%.*s", (int)(len / sizeof(wchar_t)), (const wchar_t*)buf);
 else
 printf("%.*s", (int)len, buf);
 }
 break;
 }
 ok = ContinueDebugEvent(e.dwProcessId, e.dwThreadId, continueStatus);
 assert(ok);
}

WaitForSingleObject(processInfo.hProcess, INFINITE);
CloseHandle(processInfo.hThread);
CloseHandle(processInfo.hProcess);

CloseHandle(writePipe);
CloseHandle(readPipe);

readingThread.join();

There are several codes of debug events that we can handle. There is one about process created or exited, thread created or exited, new DLL loaded, exception happening, and the one most interesting for us, providing debug string. After the switch, we need to call function ContinueDebugEvent with appropriate parameters, especially continueStatus. Some kinds of events can be ignored, but some require our attention:

EXCEPTION_DEBUG_EVENT: Because we don’t handle exceptions in any way, we should return DBG_EXCEPTION_NOT_HANDLED as the continueStatus instead the default DBG_CONTINUE.

CREATE_PROCESS_DEBUG_EVENT: Documentation of the CREATE_PROCESS_DEBUG_INFO structure says that we need to manually close the hFile handle every time we receive this event. Otherwise, we would have a leak.

EXIT_PROCESS_DEBUG_EVENT: This one can tell us when our subprocess ends, so that we can break the loop.

OUTPUT_DEBUG_STRING_EVENT: This one is called whenever our debugged program calls OutputDebugString. It carries a length in characters (nDebugStringLength) and a pointer to the data (lpDebugStringData). The way it works, as I understand it, is that there is no buffering, like we saw when using a pipe. The subprocess is paused inside the call OutputDebugString, our debugger receives this event, we need to read the original memory of the string, then we resume the subprocess by calling ContinueDebugEvent.

Because handling this type of messages is so direct, there are additional complications we need to handle:

  1. The pointer we receive is actually an address in the address space of the subprocess, not our process, so we cannot simply dereference it. We need to call ReadProcessMemory to read some data under this address.
  2. No character conversion occurs. There are two versions of the function: OutputDebugStringA and OutputDebugStringW, selected automatically when calling OutputDebugString based on character set selected in the project properties, like with other WinAPI functions. We receive the string as it was send, so we need to respect fUnicode flag and handle both types of strings – ANSI and Unicode.
  3. Posting messages using OutputDebugString works very slowly. Don’t overuse it, as it may become the performance bottleneck if you print a message about every little piece of work that your program does.

An important caveat is that WaitForDebugEventEx function must be called from the same thread that called CreateProcess. This is why our sample code executes the debugging loop on the main thread, while the loop that reads standard output stream using ReadFile is implemented in the readingThreadFunc lambda called from another thread, as in the previous example.

Summary

In this article, we learned about different ways of printing text messages from a Windows program written in C++ using Visual Studio:

We investigated how they behave when the project is configured as a console application, windows application, when the output is redirected to a file, when a console is opened explicitly using AllocConsole(), and when running under Visual Studio debugger.

Finally, we investigated how messages printed using standard output, standard error, and OutputDebugString can be captured when launching a subprocess with CreateProcess function.

Visual Studio / C++ project accompanying this article: github.com/sawickiap/TextOutputTest

Comments | #windows #winapi Share

Comments

[Download] [Dropbox] [pub] [Mirror] [Privacy policy]
Copyright © 2004-2024