I often use System.Diagnostics.Trace.WriteLine() debugging applications. In the past, if I needed to persist these statements to a log file programmatically I was using a TextWriterTraceListener object to do this. However, it turns out that TraceListeners only work for the current appdomain, which is a bit limiting.

I thought it would be cool to write a version of TextWriterTraceListener that worked more like DbgView.exe from SysInternals, i.e. that listened to Trace.WriteLine()s from all appdomains in all processes. Here is my first draft, which is basically a managed version of the DbMon sample code from MSDN. It's a console app that listens for calls to OutputDebugString() and echoes them to stdout.

Note: I've only tested this with .Net Framework 2.0 (Whidbey Beta 2). You'll probably need to tweak it significantly to get it to work with 1.0 or 1.1.

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Text;
using System.Threading;

namespace DebugMonitor
{
    unsafe static class NativeMethods
    {
        [StructLayout(LayoutKind.Sequential)]
        public struct SECURITY_ATTRIBUTES
        {
            public UInt32 nLength;
            public void* lpSecurityDescriptor;
            public UInt32 bInheritHandle;
        }

        internal const uint PAGE_READWRITE = 0x04;
        internal const uint FILE_MAP_READ = 0x0004;

        [DllImport("kernel32.dll", SetLastError = true)]
        internal static extern IntPtr CreateFileMapping(IntPtr hFile,
          SECURITY_ATTRIBUTES* lpFileMappingAttributes,
          UInt32 flProtect, UInt32 dwMaximumSizeHigh,
          uint dwMaximumSizeLow, string lpName);

        [DllImport("kernel32.dll", SetLastError = true)]
        internal static extern byte* MapViewOfFile(IntPtr hFileMappingObject, UInt32
          dwDesiredAccess, UInt32 dwFileOffsetHigh, UInt32 dwFileOffsetLow,
          UIntPtr dwNumberOfBytesToMap);

        [DllImport("kernel32.dll", SetLastError = true)]
        internal static extern bool UnmapViewOfFile(byte* lpBaseAddress);

        [DllImport("kernel32.dll", SetLastError = true)]
        internal static extern bool CloseHandle(IntPtr hHandle);
    }

    unsafe class Program
    {
        const int SharedMemorySize = 512;

        static void Main(string[] args)
        {
            EventWaitHandle ackEvent = null;
            EventWaitHandle readyEvent = null;
            IntPtr sharedFile = IntPtr.Zero;
            byte* sharedMemory = null;

            uint lastPid;
            uint* thisPid;
            byte* theString;
            bool didCR;

            bool wasCreated;

            try
            {
                EventWaitHandleSecurity ewhSec = new EventWaitHandleSecurity();
                //Note: CLR doesn't recognize S-1-0 (bug?) so I'm using S-1-1-0 instead. Seems to work.
                EventWaitHandleAccessRule rule = new EventWaitHandleAccessRule(new SecurityIdentifier("S-1-1-0"), EventWaitHandleRights.FullControl, AccessControlType.Allow);
                ewhSec.AddAccessRule(rule);

                FileSecurity fileSecurity = new FileSecurity();
                FileSystemAccessRule fileRule = new FileSystemAccessRule(new SecurityIdentifier("S-1-1-0"), FileSystemRights.FullControl, AccessControlType.Allow);
                fileSecurity.AddAccessRule(fileRule);

                byte[] fileSecurityBytes = fileSecurity.GetSecurityDescriptorBinaryForm();

                ackEvent = new EventWaitHandle(false,
                    EventResetMode.AutoReset,
                    "DBWIN_BUFFER_READY",
                    out wasCreated,
                    ewhSec);

                if (!wasCreated)
                {
                    Console.WriteLine("There is already an instance of this program or a similar monitor program (e.g. DebugView, DbMon) running.");
                    return;
                }

                readyEvent = new EventWaitHandle(false,
                    EventResetMode.AutoReset,
                    "DBWIN_DATA_READY",
                    out wasCreated,
                    ewhSec);

                fixed (byte* fileSecurityPtr = fileSecurityBytes)
                {
                    NativeMethods.SECURITY_ATTRIBUTES sa;
                    sa.bInheritHandle = 1;
                    sa.lpSecurityDescriptor = fileSecurityPtr;
                    sa.nLength = (uint)fileSecurityBytes.Length;

                    sharedFile = NativeMethods.CreateFileMapping(
                        new IntPtr(-1),
                        &sa,
                        NativeMethods.PAGE_READWRITE,
                        0,
                        4096,
                        "DBWIN_BUFFER");
                }

                if (sharedFile == IntPtr.Zero)
                {
                    Exception innerException = new Win32Exception();
                    throw new ApplicationException("CreateFileMapping failed", innerException);
                }

                sharedMemory = NativeMethods.MapViewOfFile(sharedFile, NativeMethods.FILE_MAP_READ, 0, 0, new UIntPtr(SharedMemorySize));

                if (sharedMemory == null)
                {
                    Exception innerException = new Win32Exception();
                    throw new ApplicationException("MapViewOfFile failed", innerException);
                }

                thisPid = (uint*)sharedMemory;
                theString = sharedMemory + sizeof(uint);

                lastPid = 0xffffffff;
                didCR = true;

                ackEvent.Set();

                char[] text = new char[SharedMemorySize];

                while (true)
                {
                    readyEvent.WaitOne();

                    if (lastPid != *thisPid)
                    {
                        lastPid = *thisPid;

                        if (!didCR)
                        {
                            Console.WriteLine();
                            didCR = true;
                        }
                    }

                    int length = 0;

                    fixed (char* buffer = text)
                    {
                        for (byte* p = theString; p < (sharedMemory + SharedMemorySize); ++p, ++length)
                        {
                            if (*p == 0)
                            {
                                break;
                            }
                        }
                        length = Encoding.ASCII.GetChars(theString, length, buffer, SharedMemorySize);
                    }

                    Console.Write(text, 0, length);
                    didCR = text.Length > 0 && text[text.Length - 1] == '\n';

                    ackEvent.Set();
                }
            }
            catch (Exception exn)
            {
                Console.WriteLine(exn.ToString());
            }
            finally
            {
                if (ackEvent != null)
                {
                    ackEvent.Close();
                }

                if (readyEvent != null)
                {
                    readyEvent.Close();
                }

                if (sharedMemory != null)
                {
                    NativeMethods.UnmapViewOfFile(sharedMemory);
                }

                if (sharedFile != IntPtr.Zero)
                {
                    NativeMethods.CloseHandle(sharedFile);
                }
            }
        }
    }
}