Click here to Skip to main content

WPF Trace Client and TCP Trace Listener

Introduction

This article is about a Trace client to visualize and filter Trace messages, and a TCP Trace listener.

There are many Trace listeners out there. But, there are only a few Trace clients (receivers, applications, etc.). Like that one I've used in a company I worked for once.

There is another important point: this is the first release of the Trace client application! It's not ready yet, but it does everything I need to start coding a larger application. Check out for updates at my blog and look also at the ToDo section.

Background

The Trace client is written with WPF - it's the simplest and the best way to build GUIs Smile | :) To receive Trace messages, I've implemented a TCP Trace listener. This is the best way to send large messages to another application (on the same machine or a remote one).

Each Trace listener opens a TCP socket to listen to. The Trace client connects to that socket and displays the Trace messages. If you like, you could use any Telnet application to do the same - in the next version, I'll add support for text messages; as for now, the Trace listener sends XML messages.

Using the Code

First of all: how to use it!

Trace Listener

As it is "only" a Trace listener, you use tracing as what Microsoft thinks tracing is about. Just add the Trace listener to your config file:

<system.diagnostics>
  <trace>
    <listeners>
      <add name="tcp" type="TraceClient.TcpTraceListener, TraceListener" port="666" />
    </listeners>
  </trace>
</system.diagnostics>

As the TCP Trace listener opens a TCP socket, it only needs to know which port it should listen to. This can be configured through the "port" attribute. If you are in trouble when there is more than one application domain, you can use this syntax:

port="AppDomainName:Port;DefaultPort"
e.g.: port="ServerAppDomain:666;ClientAppDomain:667;668"

This sample shows the configuration for a "Server" AppDomain, sending on port 666, a "Client" AppDomain, sending on port 667, and the default port on 668. I need such AppDomains, e.g., when my application is able to host a server and a client in the same process - very useful for single user installations.

Trace Client

On the Trace client side, you have to change the config file too. It's a simple array of address/port entries. This is a ToDo: Add a configuration dialog.

<userSettings>
  <TraceClient.Properties.Settings>
    <setting name="TraceSources" serializeAs="String">
      <value>127.0.0.1:666;127.0.0.1:667;127.0.0.1:668</value>
    </setting>
  </TraceClient.Properties.Settings>
</userSettings>

That's it...

Points of Interest

TraceListener

I don't want to explain the Trace listener in detail because there are many good articles about Trace listeners. E.g.: A TraceListener that sends messages via UDP, and Port Writer Trace Listener for .NET Applications. Those Trace listeners send Trace messages through UDP. And by the way, this article is inspired by the Port Writer Trace Listener article.

Why TCP? Well, with UPD, you can only send small packages (your sysadmin knows more). So I needed a alternative.

First, I'll read the config.

protected override string[] GetSupportedAttributes()
{
    return new string[] { "port" };
}

private int GetPort()
{
    // Get port config
    string port = this.Attributes["port"];
    if (string.IsNullOrEmpty(port))
    {
        throw new ArgumentNullException("port", "port is not set in your config.");
    }

    int result = -1;
    
    // do some parsing...
    
    return result;    
}

When a TraceRequest comes in, a TraceMessage is created and passed to the Send Method.

private StringBuilder writeBuffer = new StringBuilder();

public override void Write(string message)
{
    writeBuffer.Append(message);
}

public override void WriteLine(string message)
{
    writeBuffer.Append(message);
    Send(new TraceMessage(writeBuffer.ToString()));
    writeBuffer = new StringBuilder();
}

public override void TraceEvent(TraceEventCache eventCache, 
       string source, TraceEventType eventType, int id, string message)
{
    Send(new TraceMessage(eventCache, source, eventType, id, message));
}

public override void TraceData(TraceEventCache eventCache, 
       string source, TraceEventType eventType, int id, object data)
{
    Send(new TraceMessage(eventCache, source, eventType, id, data));
}

The writeBuffer is used to cache Write method calls.

The Send method, first of all, does a call to CreateSocket. This method opens a TCP listener. But only once! If we have trouble there, no TraceMessages will be send. Most of the troubles are related to used ports, so we don't want to try opening a port over and over again.

The reason why Send creates the socket and not the constructer is the fact that GetSupportedAttributes() is called later! So, there must be another entry point.

private static TcpListener listener = null;

private void CreateSocket()
{
    lock (typeof(TcpTraceListener))
    {
        // Only once...
        if (initialized) return;
        initialized = true;
        try
        {
            // Create listener on any IPAddress
            listener = new System.Net.Sockets.TcpListener(IPAddress.Any, GetPort());
            listener.Start();
            listener.BeginAcceptSocket(new AsyncCallback(AcceptSocket), null);
        }
        catch (Exception ex)
        {
            // Funny, isn't it
            Console.WriteLine(ex.ToString());
            Debug.Fail("Unable to create TcpListener", ex.ToString());
        }
    }
}

CreateSocket makes an asynchronous call to AcceptSocket - we don't want to block. Debug.Fail should be replaced with another option, like writing to the Event Log. But during development, a Debug.Fail is a nice thing Wink | ;-)

private static HashSet<Socket> clients = new HashSet<Socket>();

private static void AcceptSocket(IAsyncResult result)
{
    try
    {
        // Client connected, add to ClientSet.
        Socket s = listener.EndAcceptSocket(result);
        listener.BeginAcceptSocket(new AsyncCallback(AcceptSocket), null);

        clients.Add(s);
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.ToString());
    }
}

My AcceptSocket callback is just adding the result socket to a collection and starts another BeginAcceptSocket method call.

Back to the Send method:

public void Send(TraceMessage msg)
{
    try
    {
        // Creates the Socket - once!
        CreateSocket();

        // Convert TraceMessage to a MemoryStream
        MemoryStream m = new MemoryStream();
        msg.ToStream(m);

        byte[] buffer = m.GetBuffer();

        // If a client fails, remember that client. Later we will remove them.
        List<Socket> socketsToRemove = null;
        foreach (Socket s in clients)
        {
            try
            {
                // Send async
                s.BeginSend(buffer, 0, (int)m.Length, SocketFlags.None, 
                            new AsyncCallback(SendCallBack), s);
            }
            catch
            {
                // If any exception is raised remove the client
                if (socketsToRemove == null)
                {
                    socketsToRemove = new List<Socket>();
                }
                socketsToRemove.Add(s);
            }
        }

        // Close each removed client and remove it from the ClientSet
        if (socketsToRemove != null)
        {
            socketsToRemove.ForEach(s => { s.Close(); clients.Remove(s); });
        }
    }
    catch(Exception ex)
    {
        Console.WriteLine(ex.ToString());
    }
}

private static void SendCallBack(IAsyncResult result)
{
    Socket s = result.AsyncState as Socket;
    try
    {
        s.EndSend(result);
    }
    catch
    {
        // If any exception is raised remove the client
        s.Close();
        clients.Remove(s);
    }
}

The Send method serializes the TraceMessage to a MemoryStream. Then, an asynchronous call to Socket.Send is made to all clients - asynchronous because we don't want to wait. If there is any exception, then the client socket will be removed from the collection. This is done through a small Lambda expression Smile | :)

If any exception is thrown outside the loop - please do not tTrace that exception! It will end up in a recursion.

SendCallBack simply calls EndSend. Exception -> close and remove the socket from the collection.

That's it on the TraceListener side, let's look at the TraceMessage:

TraceMessage

public class TraceMessage
{
    // Properties
    public DateTime DateTime { get; set; }
    public int Id { get; set; }
    public TraceEventType EventType { get; set; }
    public string Source { get; set; }
    public string Message { get; set; }
    public int ProcessId { get; set; }
    public string ThreadId { get; set; }
    public string CallStack { get; set; }
    public string ObjectData { get; set; }
    public string MachineName { get; set; }
    public string AppDomain { get; set; }
    
    ...
}

That is a lot of properties. But there is more: ProcessID, ProcessName, MachineName, and AppDomain are cached in a static constructor.

// Internal Properties
private static int _pID;
private static string _pName;
private static string _MachineName;
private static string _AppDomain;

static TraceMessage()
{
    _pID = System.Diagnostics.Process.GetCurrentProcess().Id;
    _pName = System.Diagnostics.Process.GetCurrentProcess().ProcessName;
    _MachineName = System.Environment.MachineName;
    _AppDomain = System.AppDomain.CurrentDomain.FriendlyName;
}

We also have a serializer:

private static XmlSerializer xml = new XmlSerializer(typeof(TraceMessage));

public void ToStream(Stream s)
{
    xml.Serialize(s, this);
}

public static TraceMessage FromStream(Stream s)
{
    return (TraceMessage)xml.Deserialize(s);
}

public static TraceMessage FromStream(TextReader s)
{
    return (TraceMessage)xml.Deserialize(s);
}

All other methods are simple constructors which only applies properties.

TraceClient

The TraceClient is a WPF application. We have a TraceClientWindow (the main window), a TraceDetailWindow, a TraceMessageCollection, and a TcpTraceReceiver. Let's start with the TcpTraceReceiver.

TcpTraceReceiver

This class simply collects TraceMessages. I'll just explain the OnReceive method.

private byte[] buffer = new byte[1024];
private TcpClient client = null;
private StringBuilder sb = new StringBuilder();

public delegate void TraceMessageReceivedHandler(IPEndPoint sender, 
                                                 TraceMessage msg);
public event TraceMessageReceivedHandler OnTraceMessageReceived = null;

private void OnReceive(IAsyncResult result)
{
    try
    {
        // Get Data
        int count = client.Client.EndReceive(result);
        if (count > 0)
        {
            // Add to Message Buffer
            for (int i = 0; i < count; i++)
            {
                sb.Append((char)buffer[i]);
            }

            // See, if a TraceMessage is complete
            if (sb.ToString().Contains("</TraceMessage>"))
            {
                // Get XML
                string xml = sb.ToString();
                int lastIdx = xml.IndexOf("</TraceMessage>") + 
                                          "</TraceMessage>".Length; 
                xml = xml.Substring(0, lastIdx);

                // Remove Message from Buffer
                sb.Remove(0, lastIdx);

                // Call Received event
                if (OnTraceMessageReceived != null)
                {
                    OnTraceMessageReceived(sender, 
                        TraceMessage.FromStream(new StringReader(xml)));
                }
            }
        }

        // Receive next Message
        client.Client.BeginReceive(buffer, 0, buffer.Length, 
               SocketFlags.None, new AsyncCallback(OnReceive), null);
    }
    catch
    {
        // If any expeption is raised close the TCP Connection and try is again.
        Connect();
    }
}

It's an asynchronous callback from BeginReceive. First of all, we convert the the byte array to a StringBuilder. Next, we look for the end of our XML document (TraceMessages are sent through XML). If we find the end, then we extract the XML string and try to convert it back to a TraceMessage. Then, we fire our OnTraceMessageReceived event. At the end, we start listening for more data through BeginReceive.

Any exception, even an invalid XML document, will close the socket (through Connect).

TraceMessageCollection

This is an interesting part! The TraceMessageCollection keeps track of all Trace messages and holds a distinct list of machine names, processes, AppDomains, and thread IDs for filtering. All done through ObservableCollection. This helps us with binding the WPF-elements.

public class TraceMessageCollection : ObservableCollection<TraceMessage>
{
    ObservableCollection<Machine> _machineList = new ObservableCollection<Machine>();

    public ObservableCollection<Machine> MachineList
    {
        get
        {
            return _machineList;
        }
    }

    protected override void InsertItem(int index, TraceMessage item)
    {
        // Find the Item
        Machine m = _machineList.FirstOrDefault(i => i.Name == item.MachineName);
        if (m == null)
        {
            // Insert
            m = new Machine() { Name = item.MachineName };
            _machineList.Add(m);
        }

        // Call Machine to update Process/AppDomain and Threads
        m.Add(item);

        // Call BaseClass
        base.InsertItem(index, item);
    }
}

InsertItem looks for a Machine entry. If not found, it will create one. Then, it passes the TraceMessage to the Machine entry so that the entry can look for Processes and so on. At last, it simply adds the TraceMessage to its own collection.

public class Machine
{
    public string Name { get; set; }

    ObservableCollection<Process> _processList = 
                new ObservableCollection<Process>();

    public ObservableCollection<Process> ProcessList
    {
        get
        {
            return _processList;
        }
    }

    public void Add(TraceMessage item)
    {
        // Find the Item
        Process p = _processList.FirstOrDefault(i => i.PID == item.ProcessId);
        if (p == null)
        {
            // Insert
            p = new Process() {Machine = this, PID = item.ProcessId, 
                               Name = item.Source };
            _processList.Add(p);
        }
        // Call Process to update AppDomain and Threads
        p.Add(item);
    }
}

The Add looks for a process and so on and so on. I like the new C# 3.0 features Smile | :)

TraceClientWindow

The TraceClientWindow has a TreeView to display all the machines, processes etc., and to filter the ListView. Binding is done through the DataContext in the OnLoad event.

<Window x:Class="TraceClient.TraceClientWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Trace Client" Height="500" Width="664" 
    Loaded="Window_Loaded" Closing="Window_Closing">
    <DockPanel>
        <ToolBar DockPanel.Dock="Top">
            ...
        </ToolBar>
        <StatusBar DockPanel.Dock="Bottom" FlowDirection="RightToLeft">
            ...
        </StatusBar>
        <TreeView DockPanel.Dock="Left" Width="200" 
                  ItemsSource="{Binding Path=MachineList}"
                  SelectedItemChanged="TreeView_SelectedItemChanged">
            <TreeView.ItemTemplate>
                <HierarchicalDataTemplate ItemsSource="{Binding Path=ProcessList}">
                    <TextBlock Text="{Binding Path=Name}" />
                    <HierarchicalDataTemplate.ItemTemplate>
                        <HierarchicalDataTemplate 
                               ItemsSource="{Binding Path=AppDomainList}">
                            <TextBlock Text="{Binding}"/>
                            <HierarchicalDataTemplate.ItemTemplate>
                                <HierarchicalDataTemplate 
                                       ItemsSource="{Binding Path=ThreadList}">
                                    <TextBlock Text="{Binding Path=Name}"/>
                                    <HierarchicalDataTemplate.ItemTemplate>
                                        <DataTemplate>
                                            <TextBlock Text="{Binding Path=TID}" />
                                        </DataTemplate>
                                    </HierarchicalDataTemplate.ItemTemplate>
                                </HierarchicalDataTemplate>
                            </HierarchicalDataTemplate.ItemTemplate>
                        </HierarchicalDataTemplate>
                    </HierarchicalDataTemplate.ItemTemplate>
                </HierarchicalDataTemplate>
            </TreeView.ItemTemplate>
        </TreeView>
        <ListView Name="lstTrace" ItemsSource="{Binding}" 
                  SelectionMode="Single" MouseDoubleClick="lstTrace_MouseDoubleClick">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="DateTime" 
                         DisplayMemberBinding="{Binding Path=DateTime}" Width="150" />
                    ...
                </GridView>
            </ListView.View>
        </ListView>
    </DockPanel>
</Window>

The TreeView is interesting: it contains a HierarchicalDataTemplate bound to each parent node. So, we can declare in XAML how the TreeView should look like. As all collections are ObservableCollection, we don't have to care about updates Smile | :)

A word about the OnTraceMessageReceived event which was bound in the Window_Load event:

private void r_OnTraceMessageReceived(IPEndPoint sender, TraceMessage msg)
{
    try
    {
        // Invoke through Dispatcher
        this.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal,
            (ThreadStart)delegate { 
                // Add Message to the Collection
                messages.Add(msg); 
                // Autoscroll
                if(AutoScroll) lstTrace.ScrollIntoView(msg); 
            });
    }
    catch
    {
    }
}

As in good old WinForms, we must not make cross thread calls! So, we add the new TraceMessage through the Dispatcher because after the item is added, all controls will be rebound through WPF. We don't have to care about updates.

Autoscrolling is done through the ScrollIntoView method.

The last point: Filtering TraceMessages. I've implemented the TreeView_SelectedItemChanged event:

private void TreeView_SelectedItemChanged(object sender, 
             RoutedPropertyChangedEventArgs<object> e)
{
    try
    {
        // Filter for MachineLevel
        if (e.NewValue is Machine)
        {
            Machine m = e.NewValue as Machine;
            lstTrace.Items.Filter = 
               (i => (i as TraceMessage).MachineName == m.Name);
        }
        // Filter for ProcessLevel
        else if (e.NewValue is Process)
        {
            Process p = e.NewValue as Process;
            lstTrace.Items.Filter = 
               (i => (i as TraceMessage).MachineName == p.Machine.Name &&
                        (i as TraceMessage).ProcessId == p.PID);
        }
        // Filter for AppDomainLevel
        else if (e.NewValue is ApplicationDomain)
        {
            ApplicationDomain a = e.NewValue as ApplicationDomain;
            lstTrace.Items.Filter = 
              (i => (i as TraceMessage).MachineName == a.Process.Machine.Name &&
                       (i as TraceMessage).ProcessId == a.Process.PID &&
                       (i as TraceMessage).AppDomain == a.Name);
        }
        // Filter for ThreadLevel
        else if (e.NewValue is Thread)
        {
            Thread t = e.NewValue as Thread;
            lstTrace.Items.Filter = 
               (i => (i as TraceMessage).MachineName == t.AppDomain.Process.Machine.Name &&
                        (i as TraceMessage).ProcessId == t.AppDomain.Process.PID &&
                        (i as TraceMessage).AppDomain == t.AppDomain.Name &&
                        (i as TraceMessage).ThreadId == t.TID);
        }
        // no Filter
        else
        {
            lstTrace.Items.Filter = (i => true);
        }
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.ToString());
    }
}

The ListView is very nice. It implements a filter through a Lambda expression. Check which tree node is selected and pass the appropriate Lambda expression.

The TraceDetailWindow simply shows the TraceMessage in detail. That's all for now!

ToDo's

  • TraceClient: Add a configuration dialog.
  • TraceClient: More filters.
  • TraceClient: Add Colours to the listview.
  • TraceMessage: User-defined message types (e.g., method call).

History

Stay tuned for updates at my blog, or of course here!

  • 02.01.2008 - Initial version.

Web04 | 2.8.160204.4 | Advertise | Privacy
Copyright © CodeProject, 1999-2016
All Rights Reserved. Terms of Service