共用方式為


WCF Extensibility – Runtime

This post is part of a series about WCF extensibility points. For a list of all previous posts and planned future ones, go to the index page .

Before starting on the actual extensibility points for the WCF runtime (first post should be online tomorrow), I decided to write this quick introduction to the runtime itself. Unlike the behaviors, which are invoked when the WCF stack is being created, the runtime extensions are invoked when actual messages are being sent / received by WCF. As could be seen in the usage examples for the behaviors, they were merely used to set up some extension points in the runtime, and those are the ones which did the “real work”.

The majority of the next posts in this series will talk about these runtime extensions. Their interfaces are defined under the System.ServiceModel.Dispatcher namespace, and unlike the behaviors they don’t follow any common pattern – they’re tailored for their specific task. Some of those interfaces apply to both client and server side of communication (parameter inspector, channel initializer, etc.), some apply to the server only (instance provider, dispatch message inspector, etc.), and some to the client only (client message inspector, interactive context initializer, etc.). The interfaces section of the namespace page has a brief description of each one of the interfaces, and their blog entries will have a more detailed description for them.

Adding runtime extensions to WCF

In many cases (again, as seen in the examples for the behaviors), more than one of those extensions are needed to accomplish a task for a specific scenario. One thing that I didn’t know until a couple of days ago was exactly the order in which each of these extensibility points are called, so I decided to write a simple program, adding hooks to all of the runtime extensibility points to see what would come out. This actually helped me to understand their role in the whole message stack, and it can be used as a simple one-stop place if you’re ever wondering where to add a hook to one of the extensibility points listed in here.

  1. class MyDispatchMessageInspector : IDispatchMessageInspector
  2. {
  3.     public MyDispatchMessageInspector()
  4.     {
  5.     }
  6.  
  7.     public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
  8.     {
  9.         ColorConsole.WriteLine(ConsoleColor.Cyan, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  10.         return null;
  11.     }
  12.  
  13.     public void BeforeSendReply(ref Message reply, object correlationState)
  14.     {
  15.         ColorConsole.WriteLine(ConsoleColor.Cyan, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  16.     }
  17. }
  18.  
  19. class MyDispatchMessageFormatter : IDispatchMessageFormatter
  20. {
  21.     IDispatchMessageFormatter inner;
  22.     public MyDispatchMessageFormatter(IDispatchMessageFormatter inner)
  23.     {
  24.         this.inner = inner;
  25.     }
  26.  
  27.     public void DeserializeRequest(Message message, object[] parameters)
  28.     {
  29.         ColorConsole.WriteLine(ConsoleColor.Cyan, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  30.         this.inner.DeserializeRequest(message, parameters);
  31.     }
  32.  
  33.     public Message SerializeReply(MessageVersion messageVersion, object[] parameters, object result)
  34.     {
  35.         ColorConsole.WriteLine(ConsoleColor.Cyan, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  36.         return this.inner.SerializeReply(messageVersion, parameters, result);
  37.     }
  38. }
  39.  
  40. class MyClientMessageInspector : IClientMessageInspector
  41. {
  42.     public MyClientMessageInspector()
  43.     {
  44.     }
  45.  
  46.     public void AfterReceiveReply(ref Message reply, object correlationState)
  47.     {
  48.         ColorConsole.WriteLine(ConsoleColor.Yellow, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  49.     }
  50.  
  51.     public object BeforeSendRequest(ref Message request, IClientChannel channel)
  52.     {
  53.         ColorConsole.WriteLine(ConsoleColor.Yellow, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  54.         return null;
  55.     }
  56. }
  57.  
  58. class MyClientMessageFormatter : IClientMessageFormatter
  59. {
  60.     IClientMessageFormatter inner;
  61.     public MyClientMessageFormatter(IClientMessageFormatter inner)
  62.     {
  63.         this.inner = inner;
  64.     }
  65.  
  66.     public object DeserializeReply(Message message, object[] parameters)
  67.     {
  68.         ColorConsole.WriteLine(ConsoleColor.Yellow, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  69.         return this.inner.DeserializeReply(message, parameters);
  70.     }
  71.  
  72.     public Message SerializeRequest(MessageVersion messageVersion, object[] parameters)
  73.     {
  74.         ColorConsole.WriteLine(ConsoleColor.Yellow, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  75.         return this.inner.SerializeRequest(messageVersion, parameters);
  76.     }
  77. }
  78.  
  79. class MyDispatchOperationSelector : IDispatchOperationSelector
  80. {
  81.     public string SelectOperation(ref Message message)
  82.     {
  83.         ColorConsole.WriteLine(ConsoleColor.Cyan, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  84.         string action = message.Headers.Action;
  85.         string method = action.Substring(action.LastIndexOf('/') + 1);
  86.         return method;
  87.     }
  88. }
  89.  
  90. class MyParameterInspector : IParameterInspector
  91. {
  92.     ConsoleColor consoleColor;
  93.     bool isServer;
  94.     public MyParameterInspector(bool isServer)
  95.     {
  96.         this.isServer = isServer;
  97.         this.consoleColor = isServer ? ConsoleColor.Cyan : ConsoleColor.Yellow;
  98.     }
  99.  
  100.     public void AfterCall(string operationName, object[] outputs, object returnValue, object correlationState)
  101.     {
  102.         ColorConsole.WriteLine(this.consoleColor, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  103.     }
  104.  
  105.     public object BeforeCall(string operationName, object[] inputs)
  106.     {
  107.         ColorConsole.WriteLine(this.consoleColor, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  108.         return null;
  109.     }
  110. }
  111.  
  112. class MyCallContextInitializer : ICallContextInitializer
  113. {
  114.     public void AfterInvoke(object correlationState)
  115.     {
  116.         ColorConsole.WriteLine(ConsoleColor.Cyan, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  117.     }
  118.  
  119.     public object BeforeInvoke(InstanceContext instanceContext, IClientChannel channel, Message message)
  120.     {
  121.         ColorConsole.WriteLine(ConsoleColor.Cyan, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  122.         return null;
  123.     }
  124. }
  125.  
  126. class MyOperationInvoker : IOperationInvoker
  127. {
  128.     IOperationInvoker inner;
  129.     public MyOperationInvoker(IOperationInvoker inner)
  130.     {
  131.         this.inner = inner;
  132.     }
  133.  
  134.     public object[] AllocateInputs()
  135.     {
  136.         ColorConsole.WriteLine(ConsoleColor.Cyan, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  137.         return this.inner.AllocateInputs();
  138.     }
  139.  
  140.     public object Invoke(object instance, object[] inputs, out object[] outputs)
  141.     {
  142.         ColorConsole.WriteLine(ConsoleColor.Cyan, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  143.         return this.inner.Invoke(instance, inputs, out outputs);
  144.     }
  145.  
  146.     public IAsyncResult InvokeBegin(object instance, object[] inputs, AsyncCallback callback, object state)
  147.     {
  148.         ColorConsole.WriteLine(ConsoleColor.Cyan, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  149.         return this.inner.InvokeBegin(instance, inputs, callback, state);
  150.     }
  151.  
  152.     public object InvokeEnd(object instance, out object[] outputs, IAsyncResult result)
  153.     {
  154.         ColorConsole.WriteLine(ConsoleColor.Cyan, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  155.         return this.inner.InvokeEnd(instance, out outputs, result);
  156.     }
  157.  
  158.     public bool IsSynchronous
  159.     {
  160.         get
  161.         {
  162.             ColorConsole.WriteLine(ConsoleColor.Cyan, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  163.             return this.inner.IsSynchronous;
  164.         }
  165.     }
  166. }
  167.  
  168. class MyInstanceProvider : IInstanceProvider
  169. {
  170.     Type serviceType;
  171.     public MyInstanceProvider(Type serviceType)
  172.     {
  173.         this.serviceType = serviceType;
  174.     }
  175.  
  176.     public object GetInstance(InstanceContext instanceContext, Message message)
  177.     {
  178.         ColorConsole.WriteLine(ConsoleColor.Cyan, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  179.         return Activator.CreateInstance(this.serviceType);
  180.     }
  181.  
  182.     public object GetInstance(InstanceContext instanceContext)
  183.     {
  184.         ColorConsole.WriteLine(ConsoleColor.Cyan, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  185.         return Activator.CreateInstance(this.serviceType);
  186.     }
  187.  
  188.     public void ReleaseInstance(InstanceContext instanceContext, object instance)
  189.     {
  190.         ColorConsole.WriteLine(ConsoleColor.Cyan, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  191.     }
  192. }
  193.  
  194. class MyInstanceContextProvider : IInstanceContextProvider
  195. {
  196.     IInstanceContextProvider inner;
  197.     public MyInstanceContextProvider(IInstanceContextProvider inner)
  198.     {
  199.         this.inner = inner;
  200.     }
  201.  
  202.     public InstanceContext GetExistingInstanceContext(Message message, IContextChannel channel)
  203.     {
  204.         ColorConsole.WriteLine(ConsoleColor.Cyan, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  205.         return this.inner.GetExistingInstanceContext(message, channel);
  206.     }
  207.  
  208.     public void InitializeInstanceContext(InstanceContext instanceContext, Message message, IContextChannel channel)
  209.     {
  210.         ColorConsole.WriteLine(ConsoleColor.Cyan, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  211.         this.inner.InitializeInstanceContext(instanceContext, message, channel);
  212.     }
  213.  
  214.     public bool IsIdle(InstanceContext instanceContext)
  215.     {
  216.         ColorConsole.WriteLine(ConsoleColor.Cyan, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  217.         return this.inner.IsIdle(instanceContext);
  218.     }
  219.  
  220.     public void NotifyIdle(InstanceContextIdleCallback callback, InstanceContext instanceContext)
  221.     {
  222.         ColorConsole.WriteLine(ConsoleColor.Cyan, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  223.         this.inner.NotifyIdle(callback, instanceContext);
  224.     }
  225. }
  226.  
  227. class MyInstanceContextInitializer : IInstanceContextInitializer
  228. {
  229.     public void Initialize(InstanceContext instanceContext, Message message)
  230.     {
  231.         ColorConsole.WriteLine(ConsoleColor.Cyan, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  232.     }
  233. }
  234.  
  235. class MyChannelInitializer : IChannelInitializer
  236. {
  237.     ConsoleColor consoleColor;
  238.     public MyChannelInitializer(bool isServer)
  239.     {
  240.         this.consoleColor = isServer ? ConsoleColor.Cyan : ConsoleColor.Yellow;
  241.     }
  242.     public void Initialize(IClientChannel channel)
  243.     {
  244.         ColorConsole.WriteLine(this.consoleColor, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  245.     }
  246. }
  247.  
  248. class MyClientOperationSelector : IClientOperationSelector
  249. {
  250.     public bool AreParametersRequiredForSelection
  251.     {
  252.         get
  253.         {
  254.             ColorConsole.WriteLine(ConsoleColor.Yellow, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  255.             return false;
  256.         }
  257.     }
  258.  
  259.     public string SelectOperation(MethodBase method, object[] parameters)
  260.     {
  261.         ColorConsole.WriteLine(ConsoleColor.Yellow, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  262.         return method.Name;
  263.     }
  264. }
  265.  
  266. class MyInteractiveChannelInitializer : IInteractiveChannelInitializer
  267. {
  268.     public IAsyncResult BeginDisplayInitializationUI(IClientChannel channel, AsyncCallback callback, object state)
  269.     {
  270.         ColorConsole.WriteLine(ConsoleColor.Yellow, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  271.         Action act = new Action(this.DoNothing);
  272.         return act.BeginInvoke(callback, state);
  273.     }
  274.  
  275.     public void EndDisplayInitializationUI(IAsyncResult result)
  276.     {
  277.         ColorConsole.WriteLine(ConsoleColor.Yellow, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  278.     }
  279.  
  280.     private void DoNothing() { }
  281. }
  282.  
  283. class MyErrorHandler : IErrorHandler
  284. {
  285.     public bool HandleError(Exception error)
  286.     {
  287.         ColorConsole.WriteLine(ConsoleColor.Cyan, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  288.         return error is ArgumentException;
  289.     }
  290.  
  291.     public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
  292.     {
  293.         ColorConsole.WriteLine(ConsoleColor.Cyan, "{0}.{1}", this.GetType().Name, ReflectionUtil.GetMethodSignature(MethodBase.GetCurrentMethod()));
  294.         MessageFault messageFault = MessageFault.CreateFault(new FaultCode("FaultCode"), new FaultReason(error.Message));
  295.         fault = Message.CreateMessage(version, messageFault, "FaultAction");
  296.     }
  297. }
  298.  
  299. [ServiceContract]
  300. public interface ITest
  301. {
  302.     [OperationContract]
  303.     int Add(int x, int y);
  304.     [OperationContract(IsOneWay = true)]
  305.     void Process(string text);
  306. }
  307.  
  308. [ServiceBehavior(IncludeExceptionDetailInFaults = true)]
  309. public class Service : ITest
  310. {
  311.     public int Add(int x, int y)
  312.     {
  313.         ColorConsole.WriteLine(ConsoleColor.Green, "In service operation '{0}'", MethodBase.GetCurrentMethod().Name);
  314.  
  315.         if (x == 0 && y == 0)
  316.         {
  317.             throw new ArgumentException("This will cause IErrorHandler to be called");
  318.         }
  319.         else
  320.         {
  321.             return x + y;
  322.         }
  323.     }
  324.  
  325.     public void Process(string text)
  326.     {
  327.         ColorConsole.WriteLine(ConsoleColor.Green, "In service operation '{0}'", MethodBase.GetCurrentMethod().Name);
  328.     }
  329. }
  330.  
  331. class MyBehavior : IOperationBehavior, IContractBehavior
  332. {
  333.     public void AddBindingParameters(OperationDescription operationDescription, BindingParameterCollection bindingParameters)
  334.     {
  335.     }
  336.  
  337.     public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
  338.     {
  339.         clientOperation.Formatter = new MyClientMessageFormatter(clientOperation.Formatter);
  340.         clientOperation.ParameterInspectors.Add(new MyParameterInspector(false));
  341.     }
  342.  
  343.     public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
  344.     {
  345.         dispatchOperation.CallContextInitializers.Add(new MyCallContextInitializer());
  346.         dispatchOperation.Formatter = new MyDispatchMessageFormatter(dispatchOperation.Formatter);
  347.         dispatchOperation.Invoker = new MyOperationInvoker(dispatchOperation.Invoker);
  348.         dispatchOperation.ParameterInspectors.Add(new MyParameterInspector(true));
  349.     }
  350.  
  351.     public void Validate(OperationDescription operationDescription)
  352.     {
  353.     }
  354.  
  355.     public void AddBindingParameters(ContractDescription contractDescription, ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
  356.     {
  357.     }
  358.  
  359.     public void ApplyClientBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, ClientRuntime clientRuntime)
  360.     {
  361.         clientRuntime.ChannelInitializers.Add(new MyChannelInitializer(false));
  362.         clientRuntime.InteractiveChannelInitializers.Add(new MyInteractiveChannelInitializer());
  363.         clientRuntime.MessageInspectors.Add(new MyClientMessageInspector());
  364.         clientRuntime.OperationSelector = new MyClientOperationSelector();
  365.     }
  366.  
  367.     public void ApplyDispatchBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, DispatchRuntime dispatchRuntime)
  368.     {
  369.         dispatchRuntime.ChannelDispatcher.ChannelInitializers.Add(new MyChannelInitializer(true));
  370.         dispatchRuntime.ChannelDispatcher.ErrorHandlers.Add(new MyErrorHandler());
  371.         dispatchRuntime.InstanceContextInitializers.Add(new MyInstanceContextInitializer());
  372.         dispatchRuntime.InstanceContextProvider = new MyInstanceContextProvider(dispatchRuntime.InstanceContextProvider);
  373.         dispatchRuntime.InstanceProvider = new MyInstanceProvider(dispatchRuntime.ChannelDispatcher.Host.Description.ServiceType);
  374.         dispatchRuntime.MessageInspectors.Add(new MyDispatchMessageInspector());
  375.         dispatchRuntime.OperationSelector = new MyDispatchOperationSelector();
  376.     }
  377.  
  378.     public void Validate(ContractDescription contractDescription, ServiceEndpoint endpoint)
  379.     {
  380.     }
  381. }
  382.  
  383. static class ColorConsole
  384. {
  385.     static object syncRoot = new object();
  386.  
  387.     public static void WriteLine(ConsoleColor color, string text, params object[] args)
  388.     {
  389.         if (args != null && args.Length > 0)
  390.         {
  391.             text = string.Format(CultureInfo.InvariantCulture, text, args);
  392.         }
  393.  
  394.         lock (syncRoot)
  395.         {
  396.             Console.ForegroundColor = color;
  397.             Console.WriteLine("[{0}] {1}", DateTime.Now.ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture), text);
  398.             Console.ResetColor();
  399.         }
  400.  
  401.         Thread.Sleep(50);
  402.     }
  403.  
  404.     public static void WriteLine(string text, params object[] args)
  405.     {
  406.         Console.WriteLine(text, args);
  407.     }
  408.  
  409.     public static void WriteLine(object obj)
  410.     {
  411.         Console.WriteLine(obj);
  412.     }
  413. }
  414.  
  415. static class ReflectionUtil
  416. {
  417.     public static string GetMethodSignature(MethodBase method)
  418.     {
  419.         StringBuilder sb = new StringBuilder();
  420.         sb.Append(method.Name);
  421.         sb.Append("(");
  422.         ParameterInfo[] parameters = method.GetParameters();
  423.         for (int i = 0; i < parameters.Length; i++)
  424.         {
  425.             if (i > 0) sb.Append(", ");
  426.             sb.Append(parameters[i].ParameterType.Name);
  427.         }
  428.         sb.Append(")");
  429.         return sb.ToString();
  430.     }
  431. }
  432.  
  433. class Program
  434. {
  435.     static void Main(string[] args)
  436.     {
  437.         string baseAddress = "https://" + Environment.MachineName + ":8000/Service";
  438.         using (ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress)))
  439.         {
  440.             ServiceEndpoint endpoint = host.AddServiceEndpoint(typeof(ITest), new BasicHttpBinding(), "");
  441.             endpoint.Contract.Behaviors.Add(new MyBehavior());
  442.             foreach (OperationDescription operation in endpoint.Contract.Operations)
  443.             {
  444.                 operation.Behaviors.Add(new MyBehavior());
  445.             }
  446.  
  447.             host.Open();
  448.             ColorConsole.WriteLine("Host opened");
  449.  
  450.             using (ChannelFactory<ITest> factory = new ChannelFactory<ITest>(new BasicHttpBinding(), new EndpointAddress(baseAddress)))
  451.             {
  452.                 factory.Endpoint.Contract.Behaviors.Add(new MyBehavior());
  453.                 foreach (OperationDescription operation in factory.Endpoint.Contract.Operations)
  454.                 {
  455.                     operation.Behaviors.Add(new MyBehavior());
  456.                 }
  457.  
  458.                 ITest proxy = factory.CreateChannel();
  459.                 ColorConsole.WriteLine("Calling operation");
  460.                 ColorConsole.WriteLine(proxy.Add(3, 4));
  461.  
  462.                 ColorConsole.WriteLine("Called operation, calling it again, this time it the service will throw");
  463.                 try
  464.                 {
  465.                     ColorConsole.WriteLine(proxy.Add(0, 0));
  466.                 }
  467.                 catch (Exception e)
  468.                 {
  469.                     ColorConsole.WriteLine(ConsoleColor.Red, "{0}: {1}", e.GetType().Name, e.Message);
  470.                 }
  471.  
  472.                 ColorConsole.WriteLine("Now calling an OneWay operation");
  473.                 proxy.Process("hello");
  474.  
  475.                 ((IClientChannel)proxy).Close();
  476.             }
  477.         }
  478.  
  479.         ColorConsole.WriteLine("Done");
  480.     }
  481. }

When run, the program will show when each method of the extension interfaces are called. The server ones are written in blue, while the ones from the client side are written in yellow, to make it easier to differentiate where each trace is coming from.

Coming up

The inspection interfaces, starting with the message inspectors, then the parameter inspectors.

[Code in this post]

[Back to the index]

Comments

  • Anonymous
    August 18, 2014
    this is one of the coolest little programs I've ever seen! it's really helpful for checking out what is going on under the hood!