#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);
}
}
}