Click here to Skip to main content
15,898,222 members
Articles / Desktop Programming / XAML

Diagnostic Explorer

Rate me:
Please Sign up or sign in to vote.
4.93/5 (41 votes)
29 Nov 2010LGPL315 min read 82.8K   1.5K   120  
A .NET library and website which allows developers to expose and view arbitrary diagnostic information about their .NET processes.
#region Copyright

// Diagnostic Explorer, a .Net diagnostic toolset
// Copyright (C) 2010 Cameron Elliot
// 
// This file is part of Diagnostic Explorer.
// 
// Diagnostic Explorer is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// 
// Diagnostic Explorer is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
// 
// You should have received a copy of the GNU Lesser General Public License
// along with Diagnostic Explorer.  If not, see <http://www.gnu.org/licenses/>.
// 
// http://diagexplorer.sourceforge.net/

#endregion

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.NetworkInformation;
using System.Threading;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Configuration;

namespace DiagnosticExplorer.Web
{
	// NOTE: If you change the class name "Diagnostics" here, you must also update the reference to "Diagnostics" in Web.config.
	public class Diagnostics : IDiagnosticWebProxy
	{
		private static readonly StringComparer _ignoreCase = StringComparer.CurrentCulture;
		private static ReaderWriterLockSlim _syncLock;
		public static EventSink Events { get; private set; }

		[RateProperty(ExposeTotal = true, ExposeRate = true)]
		public static RateCounter ConfigRequests { get; set; }

		[RateProperty(ExposeTotal = true, ExposeRate = true)]
		public static RateCounter DiagnosticRequests { get; set; }

		[RateProperty(ExposeTotal = true, ExposeRate = true)]
		public static RateCounter Registrations { get; set; }

		[RateProperty(ExposeTotal = true, ExposeRate = true)]
		public static RateCounter Deregistrations { get; set; }

		private static DiagFolder _root;

		private static Timer _configWriteTimer;

		static Diagnostics()
		{
			_syncLock = new ReaderWriterLockSlim();
			Events = EventSink.GetSink("Diagnostic Service", "System");
			ConfigRequests = new RateCounter(3);
			DiagnosticRequests = new RateCounter(3);
			Registrations = new RateCounter(3);
			Deregistrations = new RateCounter(3);

			DiagnosticManager.Register(typeof (Diagnostics), "Diagnostic Service", "System");

			Trace.Listeners.Add(new TextWriterTraceListener("d:\\Diags.log"));
			Trace.AutoFlush = true;

			try
			{
				_root = DiagFolder.Deserialise(File.ReadAllText(ConfigPath));
				ResortAll();
			}
			catch (Exception ex)
			{
				Trace.WriteLine(ex);
				_root = new DiagFolder();
			}

			_configWriteTimer = new Timer(WriteConfig, null, 0, 5000);
		}

		private static void ResortAll()
		{
			DiagItemComparer comparer = new DiagItemComparer();
			foreach (DiagFolder group in _root.EnumerateAll().OfType<DiagFolder>().ToArray())
				group.Items.Sort(comparer);
		}

		private class DiagItemComparer : IComparer<DiagItem>
		{

			public int Compare(DiagItem x, DiagItem y)
			{
				if (x == null && y == null) return 0;
				if (x == null) return -1;
				if (y == null) return 1;

				if (x is DiagFolder && !(y is DiagFolder))
					return -1;

				if (y is DiagFolder && !(x is DiagFolder))
					return 1;

				int nameCompare = string.Compare(x.Name, y.Name, true);
				if (nameCompare != 0)
					return nameCompare;

				DiagProcess xProc = x as DiagProcess;
				DiagProcess yProc = y as DiagProcess;
				if (xProc != null && yProc != null)
					return xProc.ProcessId.CompareTo(yProc.ProcessId);

				return x.Id.CompareTo(y.Id);				
			}
		}

		private static void WriteConfig(object state)
		{
			TidyUpGroupConfig();
			
			EnterRead();
			try
			{
				File.WriteAllText(ConfigPath, _root.Serialise());
			}
			catch (Exception ex)
			{
				Debug.WriteLine(ex);
				System.Diagnostics.Trace.WriteLine(ex);
			}
			finally
			{
				ExitRead();
			}
		}

		public DiagProxyResponse GetDiagnostics(string id, string context)
		{
			if (string.IsNullOrEmpty(id))
				return new DiagProxyResponse { ExceptionMessage = "ProcessId not specified" };

			DiagnosticRequests.Register(1);
			DiagProcess group = _root.EnumerateAll().OfType<DiagProcess>().FindById(id);
			try
			{
				if (group == null)
					return new DiagProxyResponse { ExceptionMessage = "Client not found" };

				Binding binding = GetBindingForUri(group.Uri);

				DiagnosticClient client = new DiagnosticClient(binding, new EndpointAddress(group.Uri));
				try
				{
					DiagnosticResponse response = client.GetDiagnostics(context);
					DiagProxyResponse proxyResponse = new DiagProxyResponse(response);
					proxyResponse.Id = id;
					proxyResponse.Uri = group.Uri;
					if (string.IsNullOrEmpty(response.ExceptionMessage))
					{
						SetStatus(group.Uri, OnlineState.Online, null);
						group.ProcessId = GetProcessId(response);
					}
					else if (group != null && group.State == OnlineState.Online)
					{
						SetStatus(group.Uri, OnlineState.Offline, response.ExceptionMessage);
					}
					return proxyResponse;
				}
				finally
				{
					if (client.State == CommunicationState.Opened)
						client.Close();
				}
			}
			catch (EndpointNotFoundException)
			{
				string msg = string.Format("Could not connect to {0}", group.Uri);
				SetStatus(group.Uri, OnlineState.Offline, msg);
				return new DiagProxyResponse { ExceptionMessage = msg };
			}
			catch (Exception ex)
			{
				Trace.WriteLine(string.Format("GetDiagnostics('{0}') failed: {1}", group.Uri, ex.Message));
				SetStatus(group.Uri, OnlineState.Offline, ex.Message);
				return new DiagProxyResponse
							 {
								 ExceptionMessage = ex.GetType().Name + " " + ex.Message,
								 ExceptionDetail = ex.ToString()
							 };
			}
		}

		private int GetProcessId(DiagnosticResponse response)
		{
			if (response == null) return 0;
			if (response.PropertyBags == null) return 0;

			PropertyBag bag = response.PropertyBags.FirstOrDefault(x => x.Category == "System" && x.Name == "Environment");
			if (bag != null)
			{
				Property prop = bag.Properties.FirstOrDefault(x => x.Name == "PID");
				if (prop != null)
				{
					int pid;
					if (int.TryParse(prop.Value, out pid))
						return pid;
				}
			}
			return 0;
		}

		private Binding GetBindingForUri(string uri)
		{
			if (uri == null) throw new ArgumentNullException("uri");

			if (uri.StartsWith("net.tcp", StringComparison.CurrentCultureIgnoreCase))
			{
				NetTcpBinding binding = new NetTcpBinding();
				binding.Security.Mode = SecurityMode.None;
				binding.MaxReceivedMessageSize = int.MaxValue;
				binding.ReaderQuotas.MaxStringContentLength = int.MaxValue;
				return binding;
			}

			if (uri.StartsWith("http", StringComparison.CurrentCultureIgnoreCase))
			{
				WSHttpBinding binding = new WSHttpBinding();
				binding.Security.Mode = SecurityMode.None;
				binding.MaxReceivedMessageSize = int.MaxValue;
				binding.ReaderQuotas.MaxStringContentLength = int.MaxValue;
				return binding;
			}

			string msg = string.Format("Uri not supported: {0}", uri);
			throw new NotSupportedException(msg);
		}

		private static void SetStatus(string uri, OnlineState state, string message)
		{
			DiagProcess group = FindByUri(uri);
			if (group != null)
			{
				group.State = state;
				group.Message = message;
				if (group.State == OnlineState.Online)
					group.LastOnline = DateTime.Now;
			}
		}

		private static DiagProcess FindByUri(string uri)
		{
			return _root.EnumerateAll().FindByUri(uri);
		}

		private static DiagProcess FindByHostAndProcId(string host, int procId)
		{
			return _root.EnumerateAll().OfType<DiagProcess>()
				.Where(x => _ignoreCase.Equals(GetHost(x.Uri), host) && x.ProcessId == procId)
				.FirstOrDefault();
		}

		private static void EnterRead()
		{
			if (!_syncLock.TryEnterReadLock(1000))
			  throw new ApplicationException("Failed to enter read lock");
		}

		private static void ExitRead()
		{
			_syncLock.ExitReadLock();
		}

		private static void EnterWrite()
		{
			if (!_syncLock.TryEnterWriteLock(1000))
			  throw new ApplicationException("Failed to enter read lock");
		}

		private static void ExitWrite()
		{
			_syncLock.ExitWriteLock();
		}

		public void CreateFolder(string name, string parentId)
		{
			EnterWrite();
			try
			{
				DiagFolder grp = new DiagFolder();
				grp.Name = name;
				grp.Id = Guid.NewGuid().ToString("N");

				DiagFolder parent = GetEffectiveParent(parentId);

				parent.Items.Add(grp);
				TidyUpGroupConfig();
			}
			finally
			{
				ExitWrite();
			}
		}

		private DiagFolder GetEffectiveParent(string itemId)
		{
			DiagItem item = FindById(itemId);
			
			if (item is DiagFolder)
				return (DiagFolder)item;

			if (item != null)
				return FindParent(item) ?? _root;

			return _root;
		}

		public void ReparentItem(string id, string parentId)
		{
			EnterWrite();
			try
			{
				Debug.WriteLine(string.Format("ReparentGroup('{0}', '{1}')", id, parentId));
				DiagItem grp = FindById(id);
				DiagFolder newParent = GetEffectiveParent(parentId);

				if (grp != null && newParent != null)
				{
					DiagItem[] candidates = FindMatchingTargets(grp);
					foreach (DiagItem toMove in candidates)
					{
						DiagFolder currentParent = FindParent(toMove);
						if (currentParent != null)
							currentParent.Items.Remove(toMove);

						newParent.Items.Add(toMove);
					}
				}
				TidyUpGroupConfig();
			}
			finally
			{
				ExitWrite();
			}
		}

		public void DeleteItem(string id)
		{
			EnterWrite();
			try
			{
				DiagItem grp = FindById(id);
				if (grp != null)
				{
					DiagFolder parent = FindParent(grp);
					if (parent != null)
						parent.Items.Remove(grp);
				}
				TidyUpGroupConfig();
			}
			finally
			{
				ExitWrite();
			}
		}

		public void RegisterProcess(string parentId, string name, string uri)
		{
			EnterWrite();
			try
			{
				DiagFolder parent = GetEffectiveParent(parentId);

				DiagProcess grp = new DiagProcess();
				grp.RegistrationMode = RegistrationMode.Manual;
				grp.Id = Guid.NewGuid().ToString("N");
				grp.Name = name;
				grp.Uri = uri;
				grp.State = OnlineState.Unknown;
				parent.Items.Add(grp);

				TidyUpGroupConfig();
			}
			finally
			{
				ExitWrite();
			}
		}

		public void RenameItem(string id, string name)
		{
			EnterWrite();
			try
			{
				DiagItem item = FindById(id);
				if (item != null)
				{
					DiagItem[] candidates = FindMatchingTargets(item);
					foreach (DiagItem candidate in candidates)
						candidate.Name = name;
				}
				TidyUpGroupConfig();
			}
			finally
			{
				ExitWrite();
			}
		}

		public DiagFolder GetApplicationConfig()
		{
			try
			{
				ConfigRequests.Register(1);
				return _root;
			}
			catch (Exception ex)
			{
				Trace.WriteLine(ex);
				throw new ApplicationException(ex.ToString());
			}
		}

		private static string ConfigPath
		{
			get
			{
				string configFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;
				string dir = Path.GetDirectoryName(configFile);
				return Path.Combine(dir, "Processes.xml");
			}
		}		

		private static string GetMessageDetail(SortedList<string, string> results)
		{
			using (StringWriter writer = new StringWriter())
			{
				foreach (KeyValuePair<string, string> pair in results)
					writer.WriteLine("{0}: {1}", (pair.Key ?? "").PadRight(50, ' '), pair.Value);

				return writer.ToString();
			}
		}

		internal static RegistrationResponse Register(Registration registration)
		{
			if (registration == null) throw new ArgumentNullException("registration");
			if (string.IsNullOrEmpty(registration.Uri))
				throw new ArgumentException("Uri cannot be null", "registration");
			
			Registrations.Register(1);
			EnterWrite();
			try
			{
				Trace.WriteLine("Register " + registration);
				RegistrationResponse response = new RegistrationResponse();
				response.RenewTime = RenewTime;

				DiagProcess process = FindByHostAndProcId(GetHost(registration.Uri), registration.ProcessId);
				DiagProcess[] candidates = FindMatchingTargets(registration);
				//if (process == null)
					//process = candidates.FindByUri(registration.Uri);

				if (process == null)
					process = candidates.FirstOrDefault(x => x.State != OnlineState.Online);

				if (process == null)
				{
					process = new DiagProcess();
					process.Id = Guid.NewGuid().ToString("N");
					process.State = OnlineState.Online;
					process.RegistrationMode = RegistrationMode.Auto;
					process.Name = string.Format("{0}/{1} ({2})", GetHost(registration.Uri), registration.ProcessName, registration.UserName);
					process.ProcessName = registration.ProcessName;
					process.InstanceName = registration.InstanceName;
					process.Uri = registration.Uri ?? "NULL";
					process.ProcessId = registration.ProcessId;

					DiagFolder parent = _root;
					if (candidates.Length != 0)
					{
						process.Name = candidates[0].Name;
						parent = FindParent(candidates[0]) ?? _root;
					}
					parent.Items.Add(process);
				}

				process.Uri = registration.Uri;
				process.ProcessId = registration.ProcessId;
				SetStatus(registration.Uri, OnlineState.Online, null);

				TidyUpGroupConfig();
				return response;
			}
			finally
			{
				ExitWrite();
			}
		}

		private static TimeSpan RenewTime
		{
			get { return TimeSpan.Parse(ConfigurationManager.AppSettings["RenewTime"]); }
		}

		/// <summary>
		/// Remove any entries which are no longer needed
		/// </summary>
		private static void TidyUpGroupConfig()
		{
			//Mark as offline anything which is 5 seconds late for renewal
			TimeSpan expiryTime = TimeSpan.FromSeconds(RenewTime.TotalSeconds + 10);

			DiagProcess[] autoOnline = _root.EnumerateAll().OfType<DiagProcess>()
				.Where(x => x.State == OnlineState.Online && x.RegistrationMode == RegistrationMode.Auto)
				.ToArray();

			foreach (DiagProcess grp in autoOnline)
			{
				if (DateTime.Now - grp.LastOnline > expiryTime)
				{
					grp.State = OnlineState.Offline;
					grp.Message = "Failed to renew";
				}
			}

			//Group all items by process, instance and host
			DiagProcess[][] procs = (from x in _root.EnumerateAll().OfType<DiagProcess>()
																		where x.RegistrationMode != RegistrationMode.Manual
																		group x by new { x.ProcessName, x.InstanceName, Host = GetHost(x.Uri) } into grp
																		select grp.ToArray()).ToArray();

			//For each group, remove any excess entries which are offline
			foreach (DiagProcess[] matching in procs)
			{
				//Find the items which are no longer online
				DiagProcess[] toRemove = matching.Where(
					x => x.RegistrationMode == RegistrationMode.Auto
					&& x.State != OnlineState.Online).ToArray();

				//If all must be removed, make sure we leave just one
				if (toRemove.Length == matching.Length)
					toRemove = toRemove.Skip(1).ToArray();

				//Remove the required groups
				foreach (DiagProcess item in toRemove)
				{
					DiagFolder parent = FindParent(item);
					if (parent != null)
						parent.Items.Remove(item);
				}
			}
			ResortAll();
		}

		private static DiagFolder FindParent(DiagItem group)
		{
			return _root.EnumerateAll().OfType<DiagFolder>().FirstOrDefault(x => x.Items.Contains(group));
		}

		private static DiagItem FindById(string id)
		{
			return _root.EnumerateAll().FirstOrDefault(x => x.Id == id);
		}

		private static DiagProcess[] FindMatchingTargets(Registration registration)
		{
			return FindMatchingTargets(registration.ProcessName, registration.InstanceName, registration.Uri);
		}

		private static DiagItem[] FindMatchingTargets(DiagItem item)
		{
			DiagProcess process = item as DiagProcess;

			if (process == null) 
				return new DiagItem[] { item };

			if (process.RegistrationMode == RegistrationMode.Manual) 
				return new DiagItem[] { item };

			return FindMatchingTargets(process.ProcessName, process.InstanceName, process.Uri);
		}

		private static DiagProcess[] FindMatchingTargets(string processName, string instanceName, string uri)
		{
				return _root.EnumerateAll().OfType<DiagProcess>()
								.Where(x => x.ProcessName == processName
									&& x.InstanceName == instanceName
									&& GetHost(x.Uri) == GetHost(uri))
									.ToArray();
		}

		private static string GetHost(string callbackUrl)
		{
			if (string.IsNullOrEmpty(callbackUrl)) 
				return null;

			Uri uri = new Uri(callbackUrl);
			return uri.Host;
		}

		internal static void Deregister(Registration registration)
		{
			EnterRead();
			try
			{
				Deregistrations.Register(1);
				DiagProcess group = FindByUri(registration.Uri);
				if (group != null)
				{
					group.State = OnlineState.Offline;
					group.Message = "Offline";
				}
			}
			finally
			{
				ExitRead();
			}
			TidyUpGroupConfig();
		}


		//Bit of duplication from LoggingService.svc.cs, but can't get the silverlight client to recognise that service!
		private static ILogReader _logReader;

		private ILogReader LogReader
		{
			get
			{
				if (_logReader == null)
				{
					string ioType = ConfigurationManager.AppSettings["ILogReader"];
					_logReader = (ILogReader)Activator.CreateInstance(Type.GetType(ioType));
				}
				return _logReader;
			}
		}

		public IList<DiagnosticMsg> GetMessages(LogQuery request)
		{
			RetroDiagResponse response = new RetroDiagResponse();
			return LogReader.GetMessages(request);
		}

	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)


Written By
Software Developer
United Kingdom United Kingdom
I am a software developer originally from Auckland, New Zealand. I have lived and worked in London since 2005.

Comments and Discussions