Click here to Skip to main content
15,885,638 members
Articles / Desktop Programming / System

Tracing and Logging Technologies on Windows. Part 1 - Simple Ways Providing Information for Tracing

Rate me:
Please Sign up or sign in to vote.
5.00/5 (13 votes)
18 May 2023CPOL17 min read 7.1K   199   26   2
The series of articles covers most aspects of logging and tracing mechanisms which can be embedded into your application. It discusses simple ways of tracing and also new tracing technologies which were involved in Windows 10.
Once the created application started, it is always better to know what is happening inside your program and does it execute correctly or something went wrong. For such purposes, systems have lots of helper things, but most programmers know not many of them and use usually only one way. This article shows you the simple ways to provide information for tracing and receive how to do it in C++ and C# applications.

Table of Contents

Introduction

Basic tracing ways is good to check functionality of your application without involving something specific or digging deeper into any special technology for that.

In this topic, we review the implementation of the most popular and simple ways of providing tracing information. We also discuss moments how to extend those tracing ways and receive the tracing notifications of those methods. We find out how the debuggers and the DbgView tool works.

Console Output

Then developing applications we should think and design a way to provide some test information of the progress of execution. It is not good if the application just closed without any notification or information about what went wrong. A simple way to provide tracing information if you are developing a console application is just output information into stderr. This is the basics of what we know from the beginning of study programming.

C++
#include <stdio.h>
#include <conio.h>
int main(int argc, char* argv[])
{
    // Output into stderr device
    fprintf(stderr, "Something went wrong\n");
    _getch();
    return 0;
}

For those who prefer output with the std stream, can use std::cerr.

C++
#include <iostream>
#include <conio.h>
int main(int argc, char* argv[])
{
    using namespace std;
    // Output into stderr device
    cerr << "Something went wrong"  << endl;
    _getch();
    return 0;
}

.NET has a Console.Error object for those purposes. It is the special IO.TextWriter class which represents stderr.

C#
static void Main(string[] args)
{
    Console.Error.WriteLine("Something went wrong");
    Console.ReadKey();
}

The result execution is simply displaying text in the console window.

Image 1

Ok, right now, we already understand what may have gone wrong depending on the message we output. But, what if we do not have a console application? In that case, we can also use the console output and just create a console window in case we are required to display some information. To create console windows, the AllocConsole API is used. After allocating the console, we should reopen the stderr stream with freopen API.

C++
#include <stdio.h>
#include <windows.h>

int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,
    LPSTR lpCmdLine,int nCmdShow)
{
    FILE *out_stream = stderr;
    // Call to create console window
    if (AllocConsole())    {
        // Reopen output stderr stream 
        freopen_s(&out_stream,"CONOUT$", "w", stderr);
    }
    // Providing information message 
    fprintf(out_stream, "Something went wrong\n");
    // Just to stop execution
    MessageBoxA(NULL,"Application Quit","",MB_OK);
    return 0;
}

The std::cerr should be cleared to reset the output buffer. This is done by calling the cerr.clear() method.

You can say that “such technique with allocating console for tracing is not used anywhere” and you will be wrong, as well-known VLC player doing that:

Image 2

If you follow the menu in the picture, the console window pops-up and you can see the tracing information which is provided by the application. For example, my version of VLC does not handle video codec and it notifies that in pop-up dialog, but I can find detailed information in the console window:

Image 3

In our application, we can design that the console window will be opened from the menu or as a startup argument, or just always displaying on building debug configuration.
On a .NET Windows application calling AllocConsole API also opens the console window, but if the application is running under Visual Studio, the console window is not associated with output, and writing text into the console causes display messages in the output window.

Image 4

The regular Windows applications are executed not under Visual Studio. Once the console allocated, those methods provide correct output into the console window. Calling reset output stream, like we do in C++ implementation, is not required in .NET applications.

C#
[DllImport("kernel32.dll", CallingConvention = CallingConvention.StdCall,
SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool AllocConsole();

static void Main(string[] args)
{
    AllocConsole();
    Console.WriteLine("Something went wrong");
    MessageBox.Show("Application Quit");
}

Result of programs execution not under Visual Studio:

Image 5

Console input and output streams can be redirected which give us a way to save regular output into a file. Allocating the console window is not necessary for this. In that case, we will be calling the same API which we already mentioned: freopen. The output buffer of std::cerr should also be cleared by calling the cerr.clear() method. We can use the information which is saved in a file later to determine what was wrong.

C++
#include <stdio.h>
#include <windows.h>
#include <iostream>

int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,
    LPSTR lpCmdLine,int nCmdShow)
{
    using namespace std;
    // Redirect stderr into a file
    FILE *temp;
    freopen_s(&temp,"d:\\temp.log", "w", stderr);
    // Clear cerr buffers
    cerr.clear();
    // Providing information message 
    fprintf(stderr, "Something went wrong\n");
    // Using IO stream
    cerr << "Another message"  << endl;
    // Signal that we quit
    MessageBoxA(NULL,"Application Quit","",MB_OK);
    return 0;
}

As a result, we do not see the console window at all, but the new file d:\temp.log created after the application executes.

Image 6

.NET also allows you to redirect console output into a file. The next code displays creating a StreamWriter object and passing it as an output of the console error stream. As the result of execution, we also have the d:\temp.log file.

C#
using System;
using System.IO;
using System.Text;
using System.Windows.Forms;

class Program
{
    static void Main(string[] args)
    {
        var sw = new StreamWriter(@"d:\temp.log", true, Encoding.ASCII);
        Console.SetError(sw);
        Console.Error.WriteLine("Something went wrong");
        MessageBox.Show("Application Quit");
        sw.Dispose();
    }
}

It is also possible to get the file handle of the console output stream and use it with files API. And if there is no console of the application - then such handle is empty.

C++
#include <stdio.h>
#include <windows.h>

// Print formatted string with arguments into stderr
void TracePrint(const char * format, ...)
{
    // Get stderr Handle
    HANDLE hFile = GetStdHandle(STD_ERROR_HANDLE);
    if (hFile && hFile != INVALID_HANDLE_VALUE) {
        va_list    args;
        va_start(args, format);
        // Allocate string buffer
        int _length = _vscprintf(format, args) + 1;
        char * _string = (char *)malloc(_length);
        if (_string) {
            memset(_string, 0, _length);
            // Format string
            _vsprintf_p(_string, _length, format, args);
            __try {
                DWORD dw = 0;
                // Write resulted string
                WriteFile(hFile,_string,_length,&dw,NULL);
            } __finally {
                free(_string);
            }
        }
        va_end(args);
    }
}

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, 
    LPWSTR lpCmdLine, int nCmdShow)
{
    // Allocate console
    AllocConsole();
    // Write Message Text
    TracePrint("Something went wrong\n");
    // Just Notify that we are done
    MessageBoxA(NULL,"Application Quit","",MB_OK);
}

The example above allocates the console window and writes formatted text messages into it. This works correctly once we have the console. But if we comment the line with the call AllocConsole API, then under debugger, we can see that the output handle is invalid. So, in the application, it is possible to design output depending on settings, build or command line arguments.

Image 7

In .NET, it is also possible to fully redirect console output. To handle console output redirection necessary to create your own class and inherit it from TextWriter. In that class, it is required to override two Write methods and Encoding property. Sample of such class implementation is below:

C#
class MyConsoleWriter : TextWriter
{
    public override Encoding Encoding { get { return Encoding.Default; } }

    public MyConsoleWriter() { }

    public override void Write(string value)
    {
        IntPtr file = fopen("d:\\mylog.txt", "ab+");
        var bytes = Encoding.Default.GetBytes(value);
        fwrite(bytes, 1, bytes.Length, file);
        fclose(file);
    }

    public override void WriteLine(string value)
    {
        Write((string.IsNullOrEmpty(value) ? "" : value) + "\n");
    }
}

And after it needed to set an instance of that class as output of the console by calling Console.SetOut method.

C#
static void Main(string[] args)
{
    // Redirect Console Output
    Console.SetOut(new MyConsoleWriter());
    Console.WriteLine("Simple Console Output Message");
    MessageBox.Show("Application Quit");
}

In the result of the execution program above will be the file mylog.txt on drive d:\. You can see the content of the file in the following screenshot:

Image 8

Redirecting Console Output

Another interesting way of getting information about the application workflow is to attach the parent console from the child process, so the application provides information to its parent. So, the regular application is the Windows application and has no console window, but it uses functions for writing console output in a common way. And under the tracing mode, the application starts itself with an allocated console window, so the child process is attached to it. Let’s modify the previous code example for such implementation:

C++
int WINAPI wWinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,
    LPWSTR lpCmdLine,int nCmdShow)
{
    INT nArgs;
    // Get Command line arguments
    LPWSTR * pszArgv = CommandLineToArgvW(lpCmdLine, &nArgs);
    // Check if we starting child Process
    if (pszArgv && nArgs >= 1 && _wcsicmp(pszArgv[0], L"child") == 0) {
        // Attach to parent process console
        AttachConsole(ATTACH_PARENT_PROCESS);
        int idx = 0;
        // Simple loop for message printing
        while (idx++ < 100){
            TracePrint("Child Message #%d\n",idx);
            Sleep(100);
        }
    } else {
        // Get Path of executable
        WCHAR szPath[1024] = {0};
        GetModuleFileNameW(NULL,szPath + 1,_countof(szPath) - 1);
        szPath[0] = '\"';
        // Append Argument
        wcscat_s(szPath,L"\" child");
        // Allocate Console
        AllocConsole();

        PROCESS_INFORMATION pi = {0};
        STARTUPINFO si = {0};
        si.cb = sizeof(STARTUPINFO);
        
        // Starting child process
        if (CreateProcess(NULL, szPath, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) {
            // Wait Child Process To Quit
            WaitForSingleObject(pi.hThread, INFINITE);
            CloseHandle(pi.hProcess);
            CloseHandle(pi.hThread);
            MessageBoxA(NULL, "Application Quit", "", MB_OK);
        }
    }
    // Free Arguments List
    if (pszArgv) LocalFree(pszArgv);
    return 0;
} 

So, we have a Windows application where we allocate a console and start a child process, which is also a Windows application. In that process, we attach to the parent console and provide some messages in a loop. Once the child process is finished, the parent shows the message box.

Image 9

That is also possible to create in .NET. We just need to have a wrapper for AttachConsole and AllocConsole APIs.

C#
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Diagnostics;
using System.Windows.Forms;

class Program
{
    [DllImport("kernel32.dll", CallingConvention = CallingConvention.StdCall,
            SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool AllocConsole();

    [DllImport("kernel32.dll", CallingConvention = CallingConvention.StdCall,
            SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool AttachConsole([In,MarshalAs(UnmanagedType.U4)] int dwProcessId);

    static void Main(string[] args)
    {
        // Check if we starting child Process
        if (args.Length >= 1 && args[0].ToLower() == "child")
        {
            // Attach to parent process console
            AttachConsole(-1);
            // Do the stuff
            int idx = 0;
            // Simple loop for message printing
            while (idx++ < 100)
            {
                // Console Output Messages
                Console.WriteLine(string.Format("Child Message #{0}", idx));
                Thread.Sleep(100);
            }
        }
        else
        {
            // Allocate Console
            AllocConsole();
            // Starting child process
            var process = Process.Start(Application.ExecutablePath, "child");
            if (process != null)
            {
                process.WaitForExit();
                process.Dispose();
                MessageBox.Show("Application Quit");
            }
        }
    }
}

The result of the execution code above is the same as in C++ version.
If you don’t want to have a console window at all, then it is possible to create a pipe and replace the standard error output handle of the child process, so the parent process also receives the information. Let's modify the previous code sample to see the implementation:

C++
int WINAPI wWinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,
                              LPWSTR lpCmdLine,int nCmdShow)
{
    INT nArgs;
    LPWSTR * pszArgv = CommandLineToArgvW(lpCmdLine, &nArgs);
    // Check if we starting child Process
    if (pszArgv && nArgs >= 1 && _wcsicmp(pszArgv[0], L"child") == 0) {
        int idx = 0;
        // Simple loop for message printing
        while (idx++ < 100) {
            TracePrint("Child Message #%d\n",idx);
            Sleep(100);
        }
    } else {
        // Get Path of executable
        WCHAR szPath[1024] = {0};
        GetModuleFileNameW(NULL,szPath + 1,_countof(szPath) - 1);
        szPath[0] = '\"';
        // Append Argument
        wcscat_s(szPath,L"\" child");
        // Allocate Console
        AllocConsole();

        SECURITY_ATTRIBUTES sa = {0};
        // Setup Attributes To Inherit Handles
        sa.nLength = sizeof(SECURITY_ATTRIBUTES); 
        sa.bInheritHandle = TRUE; 
        sa.lpSecurityDescriptor = NULL; 

        HANDLE hChildInReadPipe = NULL;
        HANDLE hChildInWritePipe = NULL;
        HANDLE hChildOutReadPipe = NULL;
        HANDLE hChildOutWritePipe = NULL;
        // Create Pipes
        if (CreatePipe(&hChildOutReadPipe, &hChildOutWritePipe, &sa, 0)) {
            SetHandleInformation(hChildOutReadPipe, HANDLE_FLAG_INHERIT, 0);
            if (CreatePipe(&hChildInReadPipe, &hChildInWritePipe, &sa, 0)) {
                SetHandleInformation(hChildInWritePipe, HANDLE_FLAG_INHERIT, 0);

                PROCESS_INFORMATION pi = {0};
                STARTUPINFO si = {0};
                si.cb = sizeof(STARTUPINFO);
                // Specify Pipe Handles for stdin and stdout
                si.hStdError = hChildOutWritePipe;
                si.hStdOutput = hChildOutWritePipe;
                si.hStdInput = hChildInReadPipe;
                si.dwFlags |= STARTF_USESTDHANDLES;
                // Starting child process
                if (CreateProcess(NULL, szPath, NULL, NULL, TRUE, 0, 
                                  NULL, NULL, &si, &pi)) {
                    // Get Current stderr handle
                    HANDLE hCurrentHandle = GetStdHandle(STD_ERROR_HANDLE);
                    while (true) {
                        DWORD dwRead = 0;
                        BYTE buf[1024] = { 0 };
                        // Check for any information available on a pipe
                        if (PeekNamedPipe(hChildOutReadPipe,buf,sizeof(buf),
                            &dwRead,NULL,NULL) && dwRead) {
                            // Pull data From pipe
                            if (!ReadFile(hChildOutReadPipe, buf, 
                                sizeof(buf), &dwRead, NULL) || dwRead == 0) {
                                break;
                            }
                        }
                        // If Something readed then output it into stderr
                        if (dwRead) {
                            WriteFile(hCurrentHandle, buf, dwRead, &dwRead, NULL);
                        }
                        // Check if child process quit
                        if (WAIT_TIMEOUT != WaitForSingleObject(pi.hThread, 10)) {
                            break;
                        }
                    }
                    CloseHandle(pi.hProcess);
                    CloseHandle(pi.hThread);
                    MessageBoxA(NULL, "Application Quit", "", MB_OK);
                }
                CloseHandle(hChildInReadPipe);
                CloseHandle(hChildInWritePipe);
            }
            CloseHandle(hChildOutReadPipe);
            CloseHandle(hChildOutWritePipe);
        }
    }
    // Free Arguments List
    if (pszArgv) LocalFree(pszArgv);
    return 0;
}

In the code, we create two pairs of pipes. One pair we use as stdin and another as stdout. We pass one pipe from the pair into the child process which will be output as stdout, the second pipe from it we will be using for reading in the parent process. In the sample, we check periodically if any data is available. And, in case of new data, perform reading it and output into the console. The result of execution of this code is the same as in the previous example.

The implementation of the same scenario in .NET looks much simpler compared to C++ code.

C#
// Allocate Console
AllocConsole();
// Starting child process
var info = new ProcessStartInfo(Application.ExecutablePath, "child");
info.UseShellExecute = false;
// Redirect stdout on child process
info.RedirectStandardOutput = true;
var process = Process.Start(info);
if (process != null)
{
    // Setup data receiving callback
    process.OutputDataReceived += Child_OutputDataReceived;
    process.BeginOutputReadLine();
    // Wait for child process to quit
    process.WaitForExit();
    process.Dispose();
    MessageBox.Show("Application Quit");
}

In the code, to initiate the child process, we are using the ProcessStartInfo structure and setting the RedirectStandardOutput property value to true. Once the process object is created, it’s required to set the OutputDataReceived event handler and call BeginOutputReadLine method to start receiving data.

C#
static void Child_OutputDataReceived(object sender, DataReceivedEventArgs e)
{
    Console.WriteLine(e.Data);
}

On the callback, we just write what we receive into the previously allocated console window. Finally, we got the same output as in previous examples, but if this code started under Visual Studio, then it displayed messages in the output window.

Output Debug Information

In some cases, it is not possible to have a console view, or not possible to launch a process as a child - for example, if we have Windows Service or even Kernel Driver. In such cases, there is another easy way for providing live data of the application workflow. That is usage of Debug API. This method works for both console and window applications, also suitable for Windows services. In the application, that is done by OutputDebugString API usage.

C++
#include <stdio.h>
#include <windows.h>

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, 
    LPSTR lpCmdLine, int nCmdShow)
{
    // Output Debug Text 
    OutputDebugStringW(L"This is application workflow message\n");
    // Just Display Quit Message
    MessageBoxA(NULL,"Application Quit","",MB_OK);
} 

The text which is output with this API display in attached debuggers output window:

Image 10

An OutputDebugString API can be easily called from .NET.

C#
using System.Runtime.InteropServices;
using System.Windows.Forms;

class Program
{
    [DllImport("kernel32.dll", EntryPoint = "OutputDebugStringW", 
                CharSet = CharSet.Unicode)]
    static extern void OutputDebugString(string _text);

    static void Main(string[] args)
    {
        OutputDebugString("This is application workflow message\n");
        MessageBox.Show("Application Quit");
    }
} 

.NET also has another ability to output trace information into the attached debugger. That is involved in the System.Diagnostics namespace. There are two classes which are allowed to provide output text for the debugger: class Trace - output text under debug build or once TRACE defined and class Debug which outputs data only under debug build or once it has DEBUG definition.

C#
static void Main(string[] args)
{
    System.Diagnostics.Trace.WriteLine(
            "This is a trace message");
    System.Diagnostics.Debug.WriteLine(
        "This message displayed under debug configuration only");
    MessageBox.Show("Application Quit");
} 

You can see the result execution under debug build on output:

Image 11

You may ask questions like: what is the benefit of the usage OutputDebugString API directly if the platform already has classes which are designed for that purpose. Let's compare performance of the execution output in these two ways with the next code:

C#
[DllImport("kernel32.dll", 
            EntryPoint = "OutputDebugStringW", CharSet = CharSet.Unicode)]
static extern void OutputDebugString(string _text);

static void Main(string[] args)
{
    int iterations = 100;
    long elapsed_trace = 0;
    long elapsed_output = 0;
    {
        int idx = 0;
        long start = System.Diagnostics.Stopwatch.GetTimestamp();
        while (idx < iterations)
        {
            System.Diagnostics.Trace.WriteLine(
                string.Format("This is a trace message {0}",++idx));
        }
        elapsed_trace = System.Diagnostics.Stopwatch.GetTimestamp();
        elapsed_trace -= start;
    }
    {
        int idx = 0;
        long start = System.Diagnostics.Stopwatch.GetTimestamp();
        while (idx < iterations)
        {
            OutputDebugString(
                string.Format("This is a output debug message {0}\n",++idx));
        }
        elapsed_output = System.Diagnostics.Stopwatch.GetTimestamp();
        elapsed_output -= start;
    }
    Console.WriteLine("Time Measurments in Seconds Trace: {0}  Output: {1}", 
        elapsed_trace * 1.0 / System.Diagnostics.Stopwatch.Frequency, 
        elapsed_output * 1.0 / System.Diagnostics.Stopwatch.Frequency);

    Console.ReadKey();
} 

Well, from the code above, you can see that we measure timings of 100 iterations with the trace printing and usage debug output API. We save start timings and calculate elapsed time after the loop finished. The result of the execution is next:

Image 12

As you can see - the usage of .NET trace calls is very slow. In case of large execution in your application, you can go out for a coffee while it is processing. Of course, it is fine to use trace class for outputting some messages in non performance critical parts or if you like coffee.
Another good question - we can see our text messages only on the output window under debugger, so, how can we determine what is happening on a user PC? Do we need to install Visual Studio or any other debugger to capture outputs? The answer is - no. One good tool which can be used for that is the DbgView tool from sysinternals.

The DbgView tool properly prints outputs from the performance comparison .NET application example above, just don’t forget to run the program executable file directly, not under Visual Studio.

Image 13

As you can see, the .NET Trace class works much better to output from an application into DbgView tool instead of running under Visual Studio.

The .NET Trace class performs output information to the specified listener objects and one of them is the debugger output. Accessing the collection of the listeners causes such latency. But we can add our own listener instance which can perform any additional functionality, such as file saving. Basic our own listener implementation looks next:

C#
class MyListener : TraceListener
{
    public override void Write(string message)
    {
        OutputDebugString(message);
    }
    public override void WriteLine(string message)
    {
        Write((string.IsNullOrEmpty(message) ? "" : message) + "\n");
    }
} 

In initial listener collection, we remove all existing objects and add new instance of our own implementation:

C#
System.Diagnostics.Trace.Listeners.Clear();
System.Diagnostics.Trace.Listeners.Add(new MyListener());
System.Diagnostics.Trace.WriteLine("This is a trace message");

So, the text output in the example above will be passed to our object. That you can see if you set the breakpoint in the Write method like in the screenshot below:

Image 14

.NET has a special trace listener class ConsoleTraceListener which passes the data to the stderr or stdout.

Another class in .NET for output is the trace information into debugger. It's the static class: System.Diagnostics.Debugger.

C#
Debugger.Log(0, "Info",string.Format("This is a Debugger.Log message {0}\n", ++idx)); 

In the time measurement execution sample, we can add that Debugger class calls to see if we get better performance:

Image 15

The debug output is thread safety. That we can check by starting several threads in our application. All those threads will output text with OutputDebugString API.

C++
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, 
    LPSTR lpCmdLine, int nCmdShow)
{
    g_evQuit = CreateEvent(NULL,TRUE,FALSE,NULL);
    // Starting processint threads
    int threads = 100;
    HANDLE * processes = (HANDLE *)malloc(threads * sizeof(HANDLE));
    for (int i = 0; i < threads; i++) {
        processes[i] = CreateThread(NULL, 0, ProcessThread, NULL, 0, NULL);
    }
    Sleep(5000);
    // Set quit flag and notify threads if they are waiting
    SetEvent(g_evQuit);
    // Wait for processing threads 
    for (int i = 0; i < threads; i++) {
        WaitForSingleObject(processes[i], INFINITE);
        CloseHandle(processes[i]);
    }
    free(processes);
    CloseHandle(g_evQuit);
    MessageBoxA(NULL,"Application Quit","",MB_OK);
} 

And the ProcessThread function implementation:

C++
DWORD WINAPI ProcessThread(PVOID p)
{
    UNREFERENCED_PARAMETER(p);
    srand(GetTickCount());
    int period = rand() * 300 / RAND_MAX;
    DWORD id = GetCurrentThreadId();
    WCHAR szTemp[200] = {0};
    while (TRUE) {
        // Just writing some text into debugger until quit signal
        swprintf_s(szTemp,L"Hello from thread: %d\n",id);
        // Call Output Debug String
        OutputDebugStringW(szTemp);
        // Sleep for random selected period
        if (WAIT_OBJECT_0 == WaitForSingleObject(g_evQuit,period)) break;
    }
    return 0;
} 

You can start the application under debugger or separately. In the received results, there are no broken messages appearing in the output.

Image 16

The .NET implementation of the same functionality with the Trace.WriteLine calls also works well.

C#
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Windows.Forms;

class Program
{
    // Thread for output Trace Messages
    static void ProcessThread()
    {
        var rand = new Random();
        int id = Thread.CurrentThread.ManagedThreadId;
        int period = rand.Next(1, 300);
        do
        {
            Trace.WriteLine(string.Format("Hello From thread {0}", id));
        }
        while (!g_evQuit.WaitOne(period));
    }

    // Event For quit
    static EventWaitHandle g_evQuit = 
           new EventWaitHandle(false, EventResetMode.ManualReset);

    static void Main(string[] args)
    {
        // Create 100 threads
        List<Thread> threads = new List<Thread>();
        while (threads.Count < 100)
        {
            threads.Add(new Thread(ProcessThread));
        }
        // Start threads
        foreach (var t in threads) { t.Start(); }
        // Wait for 5 seconds
        Thread.Sleep(5000);
        // Signal to exit
        g_evQuit.Set();
        while (threads.Count > 0)
        {
            threads[0].Join();
            threads.RemoveAt(0);
        }
        MessageBox.Show("Application Quit");
    }
} 

But if you use the Debugger.Log call instead, then you will get broken messages. Which means that the Debugger.Log call is not thread safe. This is because that method split each output into separate strings.

Image 17

Receiving Output Debug Information

Let’s look at how we can receive data which was sent with the previously described methods. Output debug information internally raises an exception which can be received by the attached debugger. An exception is thrown every time API calls and if a debugger is attached, it is notified that the debug text appears. In the system, we have the IsDebuggerPresent API which allows us to determine if our application is running under debugger or not. It is possible to call OutputDebugString API only if the application is running under debugger to avoid any delays.

C++
// Check whatever we running under debugging
if (IsDebuggerPresent()) {
    // Output this text under debugger only
    OutputDebugStringW(L"This message sent if affplication under debugger\n");
} 

This API can be simply imported into .NET:

C#
[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool IsDebuggerPresent();

static void Main(string[] args)
{
    if (IsDebuggerPresent())
    {
        Console.WriteLine("We Under Debugger");
    }
    Console.ReadKey();
} 

The code above displays text if we start an application from Visual Studio. In .NET, we also have a static class Debugger with the IsAttached property which also allows us to determine that we are under debugging:

C#
if (System.Diagnostics.Debugger.IsAttached)
{
    Console.WriteLine("We Under Debugger");
} 

As mentioned earlier, current implementation of debugger string output API is done by raising the special system exception. There are two different exceptions for output debug strings. The old one is DBG_PRINTEXCEPTION_C (0x40010006) which supports only ANSI strings input. And the exception with support of the Unicode string is DBG_PRINTEXCEPTION_WIDE_C (0x4001000A). Let's try to implement our own debugger string output in the same way and see how it works.

C++
void MyOutputDebugStringW(LPCWSTR pszText) {
    if (pszText && wcslen(pszText)) {
        size_t cch = wcslen(pszText) + 1;
        // Alloc text buffer for multibyte text
        char * text = (char *)malloc(cch << 1);
        if (text) {
            __try {
                memset(text, 0x00, cch);
                size_t c = 0;
                // convert wide character into multibyte
                wcstombs_s(&c, text, cch << 1, pszText, cch - 1);

                // We need to provide both UNICODE and MBCS text
                // As old API which receive debug event work with MBCS
                ULONG_PTR args[] = {
                    cch,(ULONG_PTR)pszText,cch,(ULONG_PTR)text
                };
                __try {
                    // Raise exception which system transform 
                    // Into specified debugger event
                    RaiseException(DBG_PRINTEXCEPTION_WIDE_C,
                        0, _countof(args), args);
                }
                __except (EXCEPTION_EXECUTE_HANDLER) {
                }
            }
            __finally {
                free(text);
            }
        }
    }
} 

We are raising DBG_PRINTEXCEPTION_WIDE_C which is required to pass two different strings Ansi and Unicode. The DBG_PRINTEXCEPTION_C only requires the Ansi string argument. Which string will be provided to the debugger depends on the API it uses.
The main function implementation which calls our function.

C++
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, 
    LPSTR lpCmdLine, int nCmdShow)
{
    MyOutputDebugStringW(
        L"This text provided with own implementation of debug string output\n");
    MessageBoxA(NULL,"Application Quit","",MB_OK);
} 

The implementation can be imported into .NET with the PInvoke:

C#
[DllImport("kernel32.dll")]
static extern void RaiseException(
           [MarshalAs(UnmanagedType.U4)] int dwExceptionCode,
           [MarshalAs(UnmanagedType.U4)] int dwExceptionFlags,
           [MarshalAs(UnmanagedType.U4)] int nNumberOfArguments,
           [MarshalAs(UnmanagedType.LPArray,
            ArraySubType = UnmanagedType.SysInt, SizeParamIndex =2)]
                IntPtr[] lpArguments
           );

const int DBG_PRINTEXCEPTION_WIDE_C = 0x4001000A;

// Custom Output Debug String Implementation
static void MyOutputDebugString(string text)
{
    if (string.IsNullOrEmpty(text)) return;
    var unicode = Encoding.Unicode.GetBytes(text);
    // convert wide character into multibyte
    var ansi = Encoding.Convert(Encoding.Unicode, Encoding.Default, unicode);

    // Prepare Arguments
    IntPtr[] args = new IntPtr[4];
    args[0] = new IntPtr(text.Length + 1);
    args[1] = Marshal.AllocCoTaskMem(unicode.Length + 2);
    args[2] = new IntPtr(ansi.Length + 1);
    args[3] = Marshal.AllocCoTaskMem(ansi.Length + 1);
    // We need to provide both UNICODE and MBCS text
    // As old API which receive debug event work with MBCS
    Marshal.Copy(unicode, 0, args[1], unicode.Length);
    Marshal.Copy(ansi, 0, args[3], ansi.Length);
    try
    {
        // Raise exception which system transform 
        // Into specified debugger event
        RaiseException(DBG_PRINTEXCEPTION_WIDE_C,0, args.Length, args);
    }
    catch
    {
    }
    Marshal.FreeCoTaskMem(args[1]);
    Marshal.FreeCoTaskMem(args[3]);
} 

If we launch the code above from Visual Studio, then we can see the text in the output window:

Image 18

The exception which was thrown by OutputDebugString can be received by the debugger application using WaitForDebugEvent API. That function handles receiving Ansi strings. To allow receiving Unicode debug strings necessary to use WaitForDebugEventEx API. Those two functions have the same arguments. The exception debugger output also has some latency as the debugger application once receives a message, stops execution until it processes the debug event and calls ContinueDebugEvent API. That's why the debug output string is thread safe and the main debugger process receives all those messages.

As an example of handling output debug strings, we made an application which starts itself as a child process, and the main process attaches to it as a debugger. To determine that process started as a child, we will use command line arguments. The child process will perform output text with OutputDebugString API.

C++
INT nArgs;
LPWSTR * pszArgv = CommandLineToArgvW(lpCmdLine, &nArgs);
// Check if we starting child Process
if (pszArgv && nArgs >= 1 && _wcsicmp(pszArgv[0], L"child") == 0) {
    int idx = 0;
    // Simple loop for message printing
    while (idx++ < 100) {
        TracePrint("Child Message #%d\n", idx);
        Sleep(100);
    }
} 

And the TracePrint function is defined as follows:

C++
// Print formatted string with arguments into debug output
void TracePrint(const char * format, ...) {
    va_list    args;
    va_start(args, format);
    // Allocate string buffer
    int _length = _vscprintf(format, args) + 1;
    char * _string = (char *)malloc(_length);
    if (_string) {
        // Format string
        memset(_string, 0, _length);
        _vsprintf_p(_string, _length, format, args);
        __try {
            // Write resulted string
            OutputDebugStringA(_string);
        } __finally {
            free(_string);
        }
    }
    va_end(args);
} 

To avoid any security configuration, we set the DEBUG_ONLY_THIS_PROCESS flag during process creation.

C++
SECURITY_ATTRIBUTES sa = { 0 };
PROCESS_INFORMATION pi = { 0 };
STARTUPINFO si = { 0 };
// Get Path of executable
WCHAR szPath[1024] = { 0 };
GetModuleFileNameW(NULL, szPath + 1, _countof(szPath) - 1);
szPath[0] = '\"';
// Append Argument
wcscat_s(szPath, L"\" child");
// Setup Attributes To Inherit Handles
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.bInheritHandle = TRUE;
sa.lpSecurityDescriptor = NULL;        
si.cb = sizeof(STARTUPINFO);
// Starting child process
CreateProcess(NULL, szPath, NULL, NULL, TRUE,
        DEBUG_ONLY_THIS_PROCESS, NULL, NULL, &si, &pi); 

After we start debugging the created process and process messages which produced WaitForDebugEvent API, we are interested in the OUTPUT_DEBUG_STRING_EVENT event only. Once it is received, we copy data from memory pointed in the OUTPUT_DEBUG_STRING_INFO structure of the debug event using ReadProcessMemory API.

C++
char * p = nullptr;
size_t size = 0;
// Attach For Debug Events
if (!DebugActiveProcess(pi.dwProcessId)) {
    while (true) {
        DEBUG_EVENT _event = { 0 };
        // Waiting For Debug Event
        if (!WaitForDebugEvent(&_event, INFINITE)) {
            wprintf(L"WaitForDebugEvent Failed 0x%08x\n",GetLastError());
            break;
        }
        // We Interested in only for OutputDebugString 
        if (_event.dwDebugEventCode == OUTPUT_DEBUG_STRING_EVENT) {
            auto data = &_event.u.DebugString;
            size_t cch = data->nDebugStringLength + 2;
            // Allocate buffer
            if (cch > size) {
                p = (char *)realloc(p,cch);
                size = cch;
            }
            memset(p,0x00,size);
            cch = data->nDebugStringLength;
            if (data->fUnicode) cch *= 2;
            // Reading Output String Data
            if (ReadProcessMemory(pi.hProcess,
                data->lpDebugStringData, p,
                cch, &cch)
                ) {
                wchar_t Format[200] = {0};
                // Prepare Format String
                swprintf_s(Format,L"Process Output String: \"%%%s\"\n",
                    data->fUnicode ? L"s" : L"S");
                if (data->fUnicode) {
                    wchar_t * pwsz = (wchar_t *)p;
                    while (wcslen(pwsz) && pwsz[wcslen(pwsz) - 1] == '\n') {
                        pwsz[wcslen(pwsz) - 1] = '\0';
                    }
                }
                else {
                    while (strlen(p) && p[strlen(p) - 1] == '\n') {
                        p[strlen(p) - 1] = '\0';
                    }
                }
                // Output To Console Window
                wprintf(Format,p);
            } else {
                wprintf(L"ReadProcessMemory Failed 0x%08x\n",GetLastError());
            }
        }
        // Continue Receiving Events
        ContinueDebugEvent(_event.dwProcessId,_event.dwThreadId,DBG_CONTINUE);
        // Check if child process quit
        if (WAIT_TIMEOUT != WaitForSingleObject(pi.hThread, 10)) {
            break;
        }
    }
    // Detach Debugger
    DebugActiveProcessStop(pi.dwProcessId);
} else {
    wprintf(L"DebugActiveProcess Failed 0x%08x\n",GetLastError());
}
if (p) free(p);

Once we get debug output text, we print it into the console. Then, we call ContinueDebugEvent API to continue the process execution and wait for the next event. The result of program execution is displayed in the below screenshot:

Image 19

The .NET is not allowed to do the same in a simple way, but it is also possible to have such functionality with PInvoke. Child process just like in previous similar implementations just calls the OutputDebugString API in the loop.

C#
// Check if we starting child Process
if (args.Length >= 1 && args[0].ToLower() == "child")
{
    int idx = 0;
    // Simple loop for message printing
    while (idx++ < 100) {
        // Console Output Messages
        OutputDebugString(string.Format("Child Message #{0}", idx));
        Thread.Sleep(100);
    }
}

The main functionality is in the main process, and it looks similar to C++ implementation due to it containing native API calls.

C#
// Allocate Console
AllocConsole();
// Starting child process
var info = new ProcessStartInfo(Application.ExecutablePath, "child");
info.UseShellExecute = false;
var process = Process.Start(info);
if (process != null) {
    // Start Debugging Child Process
    if (DebugActiveProcess(process.Id)) {
        while (true) {
            DEBUG_EVENT _event;
            // Wait for debug event
            if (!WaitForDebugEvent(out _event, -1)) {
                break;
            }
            // We are interested in only debug string output
            if (_event.dwDebugEventCode == OUTPUT_DEBUG_STRING_EVENT) {
                int cb = (_event.nDebugStringLength + 1) << 1;
                // Allocate memory buffer
                IntPtr p = Marshal.AllocCoTaskMem(cb);
                if (p != IntPtr.Zero) {
                    try {
                        cb = _event.nDebugStringLength;
                        if (_event.fUnicode != 0) cb *= 2;
                        // Reading text from process memory
                        if (ReadProcessMemory(process.Handle, 
                            _event.lpDebugStringData, p, cb, out cb)) {
                            // Output Text Into allocated console window
                            Console.WriteLine(
                                "Process Output String: \"{0}\"", 
                                _event.fUnicode != 0 ? 
                                Marshal.PtrToStringUni(p) : Marshal.PtrToStringAnsi(p));
                        } else {
                            Console.WriteLine("ReadProcessMemory Failed 0x{0:x8}\n", 
                                               GetLastError());
                        }
                    }
                    finally {
                        Marshal.FreeCoTaskMem(p);
                    }
                }
            }
            // Continue debug process execution
            ContinueDebugEvent(_event.dwProcessId, _event.dwThreadId, DBG_CONTINUE);
            if (process.WaitForExit(10)) {
                break;
            }
        }
        // Stop Debugging
        DebugActiveProcessStop(process.Id);
    }
    else {
        Console.WriteLine("DebugActiveProcess Failed {0:x8}\n", GetLastError());
    }
    if (!process.HasExited)
    {
        process.Kill();
    }
    process.Dispose();
    MessageBox.Show("Application Quit");
}

Only interesting thing here is the WaitForDebugEvent function and DEBUG_EVENT structure. This structure has a union with the different structure arguments depending on the event code. As this structure is allocated by the caller, we need to handle it with enough size in function call wrapper. This can be done with allocating memory and passing it as IntPtr argument to the WaitForDebugEvent function, and later performing marshaling from pointer to structure DEBUG_EVENT. Or set up enough size in the structure itself and let it perform marshaling automatically. Initially, I made the first way and later I changed it, as I thought that it would look better.

C#
[StructLayout(LayoutKind.Sequential)]
struct DEBUG_EVENT
{
    [MarshalAs(UnmanagedType.U4)]
    public int dwDebugEventCode;
    [MarshalAs(UnmanagedType.U4)]
    public int dwProcessId;
    [MarshalAs(UnmanagedType.U4)]
    public int dwThreadId;
    public IntPtr lpDebugStringData;
    [MarshalAs(UnmanagedType.U2)]
    public ushort fUnicode;
    [MarshalAs(UnmanagedType.U2)]
    public ushort nDebugStringLength;
    [MarshalAs(UnmanagedType.ByValArray,SizeConst = 144)]
    public byte[] Reserved;
}

As you can see in the DEBUG_EVENT, we handle only debug text information from the OUTPUT_DEBUG_STRING_INFO structure. For other union structures, we add the padding array of bytes, so we are able to pass the structure directly in the WaitForDebugEvent API.

C#
[DllImport("kernel32.dll", CallingConvention = CallingConvention.StdCall,
SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool WaitForDebugEvent(
    [Out] out DEBUG_EVENT lpDebugEvent, 
    [In, MarshalAs(UnmanagedType.U4)] int dwMilliseconds
    );

The output of the application is the same as in C++ version.

System Wide String Output

If we launch a compiled version of the executable which outputs a string by raising an exception method and without a debugger attached, then we will not be able to see output messages which are provided with the above method by just raising the specific exception. But if you use a particular OutputDebugString API, then you can see the output text in DbgView tool. That tool is not attached as a debugger to the application. This means that if no debugger is attached, then the OutputDebugString API works in a different way.
That way has already been described many times, but I will also mention it here briefly. The OutputDebugString API internally opens the special mutex with the name DBWinMutex. By locking that mutex application opens other shared objects: two events and shared memory. After application writes output string into shared memory with name DBWIN_BUFFER, while doing that, it also waits for buffer shared event DBWIN_BUFFER_READY and once text is written, it signals the DBWIN_DATA_READY shared event. Those named objects you can see in the Process Explorer tool (ProcExp) from sysinternals. We have internally two waiting operations which can cause little latency.

Image 20

The sample of implementation providing system wide text messages based on algorithm description above is next.

C++
// Opens the mutex for accessing shared objects
HANDLE hMutex = OpenMutexW(READ_CONTROL | SYNCHRONIZE | 
                           MUTEX_MODIFY_STATE, TRUE,L"DBWinMutex");
if (!hMutex) {
    CreateSecurityDescriptor();
    SECURITY_ATTRIBUTES sa = { 0 };
    sa.nLength = sizeof(sa);
    sa.bInheritHandle = FALSE;
    sa.lpSecurityDescriptor = (LPVOID)g_pSecurityDescriptor;
    hMutex = CreateMutexExW(&sa, L"DBWinMutex", 0,
        MAXIMUM_ALLOWED | SYNCHRONIZE | READ_CONTROL | MUTEX_MODIFY_STATE);
}
if (hMutex) {
    // Provide system wide debug text
    HANDLE hMap = NULL;
    HANDLE hBufferReady = NULL;
    HANDLE hDataReady = NULL;
    // Wait for shared mutex
    DWORD result = WaitForSingleObject(hMutex, 10000);
    if (result == WAIT_OBJECT_0) {
        __try {
            // Open shared objects
            hMap = OpenFileMappingW(FILE_MAP_WRITE, FALSE, L"DBWIN_BUFFER");
            hBufferReady = OpenEventW(SYNCHRONIZE, FALSE, L"DBWIN_BUFFER_READY");
            hDataReady = OpenEventW(EVENT_MODIFY_STATE, FALSE, L"DBWIN_DATA_READY");
        }
        __finally {
            if (!hDataReady) {
                ReleaseMutex(hMutex);
            }
        }
        __try {
            LPVOID pBuffer = NULL;
            if (hMap && hBufferReady && hDataReady) {
                // Map section buffer 
                pBuffer = MapViewOfFile(hMap, SECTION_MAP_WRITE | 
                                        SECTION_MAP_READ, 0, 0, 0);
            }
            if (pBuffer) {
                cch = strlen(text) + 1;
                char * p = text;
                while (cch > 0) {
                    // Split message as shared buffer have 4096 bytes length
                    size_t length = 4091;
                    if (cch < length) {
                        length = cch;
                    }
                    // Wait for buffer to be free
                    if (WAIT_OBJECT_0 == WaitForSingleObject(hBufferReady, 10000)) {
                        // First 4 bytes is the process ID
                        *((DWORD*)pBuffer) = (DWORD)GetCurrentProcessId();
                        memcpy((PUCHAR)pBuffer + sizeof(DWORD), p, length);
                        // Append string end character for large text
                        if (length == 4091) ((PUCHAR)pBuffer)[4095] = '\0';
                        // Notify that message is ready
                        SetEvent(hDataReady);
                    }
                    else {
                        break;
                    }
                    cch -= length;
                    p += length;
                }
                // Unmap shared buffer
                UnmapViewOfFile(pBuffer);
            }
        }
        __finally {
            if (hBufferReady) CloseHandle(hBufferReady);
            if (hDataReady) {
                CloseHandle(hDataReady);
                ReleaseMutex(hMutex);
            }
            if (hMap) CloseHandle(hMap);
        }
    }
    CloseHandle(hMutex);
    FreeSecurityDescriptor();
} 

Add that implementation into our own debug output string described previously and start the application not under Visual Studio. In the DbgView tool, you can see the output of our text.

Image 21

Based on those descriptions, we can create fully functional our own OutputDebugString API.
The main issue in the implementation is the security of opening shared mutex, as OutputDebugString API can be called from non admin users.
This implementation can also easily be done in .NET. The first step is opening the mutex object.

C#
Mutex mutex = null;
// Open Shared Mutex
if (!Mutex.TryOpenExisting("DBWinMutex", 
    MutexRights.Synchronize | MutexRights.ReadPermissions | 
                              MutexRights.Modify, out mutex))
{
    bool bCreateNew;
    MutexSecurity security = new MutexSecurity();
    string CurrentUser = Environment.UserDomainName + "\\" + Environment.UserName;
    // Allow current user
    security.AddAccessRule(new MutexAccessRule(
        CurrentUser, MutexRights.Synchronize | MutexRights.Modify,
        AccessControlType.Allow));

    // Allow Any users
    security.AddAccessRule(new MutexAccessRule(
        new SecurityIdentifier("S-1-1-0"), MutexRights.Synchronize | MutexRights.Modify,
        AccessControlType.Allow));

    // Local System Read and modify
    security.AddAccessRule(new MutexAccessRule(
        new SecurityIdentifier("S-1-5-18"), 
            MutexRights.ReadPermissions | MutexRights.Modify,
        AccessControlType.Allow));

    // Admins Read and modify
    security.AddAccessRule(new MutexAccessRule(
        new SecurityIdentifier("S-1-5-32-544"), 
            MutexRights.ReadPermissions | MutexRights.Modify,
        AccessControlType.Allow));

    mutex = new Mutex(false, "DBWinMutex", out bCreateNew, security);
} 

Next is the opening shared objects.

C#
EventWaitHandle DataReady = null;
EventWaitHandle BufferReady = null;
MemoryMappedFile MappedFile = null;
MemoryMappedViewAccessor Accessor = null;

// Open Events 
if (EventWaitHandle.TryOpenExisting("DBWIN_BUFFER_READY", 
                    EventWaitHandleRights.Synchronize, out BufferReady))
{
    if (EventWaitHandle.TryOpenExisting("DBWIN_DATA_READY", 
                        EventWaitHandleRights.Modify, out DataReady))
    {
        // Open Shared Section
        try
        {
            MappedFile = MemoryMappedFile.OpenExisting
                         ("DBWIN_BUFFER", MemoryMappedFileRights.Write);
        }
        catch
        {
        }
    }
}
if (MappedFile != null) {
    // Map View 
    Accessor = MappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.ReadWrite);
}

Wait for buffer ready event signal and write data.

C#
int offset = 0;
int count = 4091;
// While we have data 
while (ansi.Length - offset > 0)
{
    // Split it 
    if (count > ansi.Length - offset) count = ansi.Length - offset;
    // Wait Buffer Access
    if (BufferReady.WaitOne(10000))
    {
        // PID
        Accessor.Write(0, PID);
        // Ansi Text
        Accessor.WriteArray<byte>(4, ansi, offset, count);
        // Zero ending string if any
        Accessor.Write(count + 4, 0);
        // Signal that we are ready
        DataReady.Set();
    }
    else
    {
        break;
    }
    offset += count;
} 

Clean up objects.

C#
if (Accessor != null) Accessor.Dispose();
if (BufferReady != null) BufferReady.Dispose();
if (DataReady != null) DataReady.Dispose();
if (MappedFile != null) MappedFile.Dispose();
if (mutex != null)
{
    mutex.ReleaseMutex();
    mutex.Dispose();
}

Result of function call you also can see in the DbgView application.

Image 22

Receiving System Wide String Output

Now it’s time to implement the receiving part. Receiving shared strings output can be done in a backward way. Initially, we open shared objects but with different access requests, as now we need to wait for the DBWIN_DATA_READY event to be signalled and reset the DBWIN_BUFFER_READY event once we are done. As you can see in the previous implementation, we are opening existing shared objects. This is done as if there is no application which is waiting for the data, then we just skip sending. So the receiver part should create instances of those objects in case they failed to be opened.

C++
// Open or create shared objects
hMap = OpenFileMappingW(FILE_MAP_READ, FALSE, L"DBWIN_BUFFER");
if (!hMap) {
    hMap = CreateFileMappingW(INVALID_HANDLE_VALUE,&sa,
           PAGE_READWRITE,0,0x1000,L"DBWIN_BUFFER");
    if (hMap == INVALID_HANDLE_VALUE) {
        hMap = NULL;
    }
}
hBufferReady = OpenEventW(EVENT_MODIFY_STATE, FALSE, L"DBWIN_BUFFER_READY");
if (!hBufferReady) {
    hBufferReady = CreateEventW(&sa,FALSE,FALSE,L"DBWIN_BUFFER_READY");
    if (hBufferReady == INVALID_HANDLE_VALUE) {
        hBufferReady = NULL;
    }
}
hDataReady = OpenEventW(SYNCHRONIZE, FALSE, L"DBWIN_DATA_READY");
if (!hDataReady) {
    bCreated = TRUE;
    hDataReady = CreateEventW(&sa,FALSE,FALSE,L"DBWIN_DATA_READY");
    if (hDataReady == INVALID_HANDLE_VALUE) {
        hDataReady = NULL;
    }
} 

Next in a loop, we wait for the event and read the data from the mapped shared section. Section can be mapped for reading access only, as we do not write any data.

C++
LPVOID pBuffer = MapViewOfFile(hMap, SECTION_MAP_READ, 0, 0, 0);
HANDLE hHandles[] = { hDataReady,g_hQuit };
while (true) {
    // Wait for event appear or quit
    if (WAIT_OBJECT_0 == WaitForMultipleObjects(
        _countof(hHandles), hHandles, FALSE, INFINITE)) {
        // First 4 bytes is the process ID
        DWORD ProcessId = *((PDWORD)pBuffer);
        // Copy data from the shared memory
        strncpy_s(text, cch, (char*)pBuffer + 4, cch - 4);
        // Notify that we are done
        SetEvent(hBufferReady);
        if (strlen(text)) {
            while (text[strlen(text) - 1] == '\n') {
                text[strlen(text) - 1] = '\0';
            }
            // Output Text
            printf("[%d] %s\n", ProcessId, text);
        }
    }
    else {
        break;
    }
}
UnmapViewOfFile(pBuffer); 

To test, we can start the previous code sample which writes output debug strings in with several threads along with our current receiver implementation and checks the result.

Image 23

We can do the same way in .NET. First, open or create the mutex object.

C#
Mutex mutex = null;
// Open or create mutex
if (!Mutex.TryOpenExisting("DBWinMutex", MutexRights.Synchronize 
            | MutexRights.ReadPermissions | MutexRights.Modify, out mutex))
{
    bool bCreateNew;
    MutexSecurity security = new MutexSecurity();
    string CurrentUser = Environment.UserDomainName + "\\" + Environment.UserName;
    // Allow current user
    security.AddAccessRule(new MutexAccessRule(
        CurrentUser, MutexRights.Synchronize | MutexRights.Modify,
        AccessControlType.Allow));

    // Allow Any users
    security.AddAccessRule(new MutexAccessRule(
        new SecurityIdentifier("S-1-1-0"), 
            MutexRights.Synchronize | MutexRights.Modify,
        AccessControlType.Allow));

    // Local System Read and modify
    security.AddAccessRule(new MutexAccessRule(
        new SecurityIdentifier("S-1-5-18"), 
            MutexRights.ReadPermissions | MutexRights.Modify,
        AccessControlType.Allow));

    // Admins Read and modify
    security.AddAccessRule(new MutexAccessRule(
        new SecurityIdentifier("S-1-5-32-544"), 
            MutexRights.ReadPermissions | MutexRights.Modify,
        AccessControlType.Allow));

    mutex = new Mutex(false, "DBWinMutex", out bCreateNew, security);
} 

After opening or creating shared objects:

C#
EventWaitHandle DataReady = null;
EventWaitHandle BufferReady = null;
MemoryMappedFile MappedFile = null;
MemoryMappedViewAccessor Accessor = null;

MemoryMappedFileSecurity memory_security = new MemoryMappedFileSecurity();
EventWaitHandleSecurity event_security = new EventWaitHandleSecurity();

memory_security.AddAccessRule(new AccessRule<MemoryMappedFileRights>(
    new SecurityIdentifier("S-1-1-0"), MemoryMappedFileRights.ReadWrite,
    AccessControlType.Allow));

event_security.AddAccessRule(new EventWaitHandleAccessRule(
    new SecurityIdentifier("S-1-1-0"), 
        EventWaitHandleRights.Synchronize | 
        EventWaitHandleRights.Modify | EventWaitHandleRights.ReadPermissions,
    AccessControlType.Allow));

// Open Buffer Event
if (!EventWaitHandle.TryOpenExisting("DBWIN_BUFFER_READY", 
     EventWaitHandleRights.Modify, out BufferReady))
{
    BufferReady = new EventWaitHandle(false, EventResetMode.AutoReset, 
                  "DBWIN_BUFFER_READY", out bCreateNew, event_security);
}
// Open Data Event
if (!EventWaitHandle.TryOpenExisting("DBWIN_DATA_READY", 
     EventWaitHandleRights.Synchronize, out DataReady))
{
    DataReady = new EventWaitHandle(false, EventResetMode.AutoReset, 
                "DBWIN_DATA_READY", out bCreateNew, event_security);
}
// Open Shared Section
try
{
    MappedFile = MemoryMappedFile.OpenExisting
                 ("DBWIN_BUFFER", MemoryMappedFileRights.Read);
}
catch
{
}
if (MappedFile == null)
{
    MappedFile = MemoryMappedFile.CreateOrOpen("DBWIN_BUFFER", cch,
        MemoryMappedFileAccess.ReadWrite, 
        MemoryMappedFileOptions.None, memory_security, 
        System.IO.HandleInheritability.None);
}
if (MappedFile != null)
{
    // Map View 
    Accessor = MappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read);
} 

Processing notifications in a loop and output messages to the console window.

C#
while (true)
{
    // Wait for event appear or quit
    if (0 == WaitHandle.WaitAny(new WaitHandle[] { DataReady, g_evQuit }))
    {
        // First 4 bytes is the process ID
        int ProcessId = Accessor.ReadInt32(0);
        // Copy data from the shared memory
        Accessor.ReadArray<byte>(4, text, 0, text.Length - 4);
        // Notify that we are done
        BufferReady.Set();
        int length = 0;
        while (length < text.Length && text[length] != '\0') length++;
        string ansi_text = Encoding.Default.GetString(text, 0, length);
        if (!string.IsNullOrEmpty(ansi_text))
        {
            if (ansi_text[ansi_text.Length - 1] == '\n') 
                ansi_text = ansi_text.Remove(ansi_text.Length - 1,1);
            Console.WriteLine("[{0}] {1}", ProcessId, ansi_text);
        }
    }
    else
    {
        break;
    }
} 

Result of execution is the same as in C++ application version.
There will be concurrency in case two instances of the receiver applications are running. So the messages can be received by one application only as in implementation auto reset events are used for synchronization. Due to that reason, the DbgView application blocks receiving data in its second instance.

Image 24

History

  • 18th May, 2023: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Russian Federation Russian Federation
I'm a professional multimedia developer (more than 10 years) in any kind of applications and technologies related to it, such as DirectShow, Direct3D, WinMM, OpenGL, MediaFoundation, WASAPI, Windows Media and other including drivers development of Kernel Streaming, Audio/Video capture drivers and audio effects. Have experience in following languages: C, C++, C#, delphi, C++ builder, VB and VB.NET. Strong knowledge in math and networking.

Comments and Discussions

 
GeneralMy vote of 5 Pin
BillWoodruff18-Aug-23 12:59
professionalBillWoodruff18-Aug-23 12:59 
GeneralRe: My vote of 5 Pin
Maxim Kartavenkov18-Aug-23 22:41
Maxim Kartavenkov18-Aug-23 22:41 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.