Saturday, July 03, 2010 2:45 PM by roger

In my last post I promised to tell you more about the performance counters. So here we are: today I'm going to tell you about how to use the performance counter infrastructure offered by Windows in a .net application. On top of this I'm providing you with a basic framework that you can use to author performance counters through XML and then get the code to read/write the counters generated for you at build time. So let's get started.

Some Basics

Before I go off to the code, here's a little overview on performance counters in Windows. I'm sure if you're reading this, you already know about Perfmon.exe, a nice little tool to look at various performance aspects of Windows machines. When plottig performance counters, you're typically adding the counters you're interested in. These counters are grouped in categories, for instance Processor or PhsyicalDisk. Each category can contain multiple counters, for instance Disk Read Bytes/sec or Disk Write Bytes/sec. And finally, each counter can have multiple instances, for instance in multi-processor machines, you'll find one instance of the % Idle Time counter per processor.

What tools like Perfmon.exe do is to grab the values of the counters you selected every second (by default, but that can typically be changed) and record/plot the values. Your job here is to build performance counters that measure certain aspects of your application so these tools can help you in analyzing your performance. And that's why you're here, right?

Step 1: The XML Schema

In order to know what we talk about in the XML I announced, let me start with the XML schema. I use this mainly to make sure that what I have in the XML declaring the perf counters makes some sense and is legal input. And of course because Visual Studio tells you that something's wrong when the schema is not adhered to.

 1: <?xml version="1.0" encoding="utf-8"?>
 2: <xs:schema targetNamespace="urn:Cymbeline.Diagnostics.PerfCounters"
 3:            xmlns="urn:Cymbeline.Diagnostics.PerfCounters"
 4:            xmlns:tns="urn:Cymbeline.Diagnostics.PerfCounters"
 5:            xmlns:xs="http://www.w3.org/2001/XMLSchema"
 6:            attributeFormDefault="unqualified"
 7:            elementFormDefault="qualified">
 8:   <xs:element name="PerfCounters">
 9:     <xs:complexType>
10:       <xs:sequence>
11:         <xs:element name="Category" maxOccurs="unbounded">
12:           <xs:complexType>
13:             <xs:sequence>
14:               <xs:element name="Counter" maxOccurs="unbounded">
15:                 <xs:complexType>
16:                   <xs:attribute name="Name" type="xs:string" use="required" />
17:                   <xs:attribute name="Symbol" type="tns:Symbol" use="required" />
18:                   <xs:attribute name="Type" use="required">
19:                     <xs:simpleType>
20:                       <xs:restriction base="xs:string">
21:                         <xs:enumeration value="NumberOfItems32" />
22:                         <xs:enumeration value="RateOfCountsPerSecond32" />
23:                         <xs:enumeration value="RawFraction" />
24:                       </xs:restriction>
25:                     </xs:simpleType>
26:                   </xs:attribute>
27:                   <xs:attribute name="Help" type="xs:string" use="required" />
28:                 </xs:complexType>
29:               </xs:element>
30:             </xs:sequence>
31:             <xs:attribute name="Name" type="xs:string" use="required" />
32:             <xs:attribute name="Symbol" type="tns:Symbol" use="required" />
33:             <xs:attribute name="Help" type="xs:string" use="required" />
34:           </xs:complexType>
35:         </xs:element>
36:       </xs:sequence>
37:     </xs:complexType>
38:   </xs:element>
39:   <xs:simpleType name="Symbol">
40:     <xs:restriction base="xs:token">
41:       <xs:pattern value="[a-zA-Z_][\w_]*" />
42:     </xs:restriction>
43:   </xs:simpleType>
44: </xs:schema>

On lines 21 to 23 you find some enumeration values. These map to the values defined in the PerformanceCounterType Enumeration. This is also where you can add support for more performance counter types when you need it. The other elements and attributes are used to describe the category for the performance counters and the performance counters themselves, including the help text that will show up in Perfmon.exe and also including the symbol that you can use in the code to access the counter.

Step 2: Declaring the Performance Counters

Now let's use the schema we built above. To give you an idea of the context, I'm providing here some of the XML I use to build the perf counters for my SMTP server.

 1: <?xml version="1.0" encoding="utf-8" ?>
 2: <PerfCounters xmlns="urn:Cymbeline.Diagnostics.PerfCounters">
 3:   <Category Name="CymbeMail" Symbol="CymbeMail" Help="CymbeMail SMTP Server v1">
 4:     <Counter Name="# Total Connections"
 5:          Symbol="TotalConnections"
 6:          Type="NumberOfItems32"
 7:          Help="The total number of connections since the server was started." />
 8:     <Counter Name="# Total Connections Refused"
 9:          Symbol="TotalRefusedConnections"
10:          Type="NumberOfItems32"
11:          Help="The total number of connections refused since the server was started." />
12:     <Counter Name="# Total Connections with Errors"
13:          Symbol="TotalErroneousConnections"
14:          Type="NumberOfItems32"
15:          Help="The total number of connections which reported errors since the server was started." />
16:     <Counter Name="# Active Connections"
17:          Symbol="ActiveConnections"
18:          Type="NumberOfItems32"
19:          Help="The number of currently active connections." />
20:     <Counter Name="# Authorization Records"
21:          Symbol="AuthorizationRecords"
22:          Type="NumberOfItems32"
23:          Help="The number of authorization records currently kept in the server." />
24:     <Counter Name="# Connections/sec"
25:          Symbol="ConnectionsPerSec"
26:          Type="RateOfCountsPerSecond32"
27:          Help="The number of connections per second." />
28:     <Counter Name="# Refused Connections/sec"
29:          Symbol="RefusedConnectionsPerSec"
30:          Type="RateOfCountsPerSecond32"
31:          Help="The number of connections refused per second." />
32:   </Category>
33: </PerfCounters>

As you can see, I create seven perf counters, most of them ordinary counters that count the number of occurrences of a certain event (like when a client makes a connection to the server). I actually also use the NumberOfItems32 type to count the number of active connections — I basically increment the counter when a connection was established and decrement it when the connection is terminated. And I have a couple of counters which count the number of occurrences per second (the RateOfCountsPerSecond32 counter type). The good thing about these counters is that you don't need to provide your own counter base (check out MSDN for more info on this, starting with the above mentioned PerformanceCounterType Enumeration).

Step 3: Building the XSLT to Generate Code

We already have our perf counters declared, so let's generate some code from that XML. Using XSL stylesheets makes this very easy: We'll use the XML as input and get c# code as output that we can simply include into our project. I won't show the full XSLT here (it's about 240 lines).

  1: <?xml version="1.0" encoding="utf-8"?>
  2: <xsl:stylesheet version="1.0"
  3:         xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  4:         xmlns:msxsl="urn:schemas-microsoft-com:xslt"
  5:         xmlns:tns="urn:Cymbeline.Diagnostics.PerfCounters"
  6:         exclude-result-prefixes="msxsl">
  7:   <xsl:output method="text" indent="no"/>
  8:  
  9:   <xsl:param name="TargetNamespace" />
 10:   <xsl:param name="TargetClassName" select="'PerfCounters'" />
 11:   <xsl:param name="AccessModifier" select="'public'" />
 12:  
 13:   <xsl:template match="tns:PerfCounters">
 14:     <xsl:if test="$TargetNamespace=''">
 15:       <xsl:message terminate="yes">
 16:         The Parameter 'TargetNamespace' is undefined.
 17:       </xsl:message>
 18:     </xsl:if>
 19:     <!-- ... -->
 20:   </xsl:template>
 21:  
 22:   <xsl:template match="tns:Counter" mode="Counter">
 23:     <xsl:text>
 24:       private static PerformanceCounter _</xsl:text><xsl:value-of select="@Symbol"/><xsl:text>;
 25:  
 26:       </xsl:text><xsl:value-of select="$AccessModifier"/><xsl:text> static PerformanceCounter </xsl:text><xsl:value-of select="@Symbol"/><xsl:text>
 27:       {
 28:         get
 29:         {
 30:           MakeSureCountersAreInitialized();
 31:           return _</xsl:text><xsl:value-of select="@Symbol"/><xsl:text>;
 32:         }
 33:       }
 34: </xsl:text>
 35:   </xsl:template>
 36:  
 37:   <xsl:template match="tns:Counter[@Type='RawFraction']" mode="Counter">
 38:     <xsl:text>
 39:       private static PerformanceCounter _</xsl:text><xsl:value-of select="@Symbol"/><xsl:text>;
 40:  
 41:       </xsl:text><xsl:value-of select="$AccessModifier"/><xsl:text> static PerformanceCounter </xsl:text><xsl:value-of select="@Symbol"/><xsl:text>
 42:       {
 43:         get
 44:         {
 45:           MakeSureCountersAreInitialized();
 46:           return _</xsl:text><xsl:value-of select="@Symbol"/><xsl:text>;
 47:         }
 48:       }
 49:  
 50:       private static PerformanceCounter _</xsl:text><xsl:value-of select="@Symbol"/><xsl:text>Base;
 51:  
 52:       </xsl:text><xsl:value-of select="$AccessModifier"/><xsl:text> static PerformanceCounter </xsl:text><xsl:value-of select="@Symbol"/><xsl:text>Base
 53:       {
 54:         get
 55:         {
 56:           MakeSureCountersAreInitialized();
 57:           return _</xsl:text><xsl:value-of select="@Symbol"/><xsl:text>Base;
 58:         }
 59:       }
 60: </xsl:text>
 61:   </xsl:template>
 62:  
 63:   <xsl:template match="tns:Counter" mode="InitCounter">
 64:     <xsl:text>
 65:         _</xsl:text><xsl:value-of select="@Symbol"/><xsl:text> = new PerformanceCounter(
 66:           CategoryName,
 67:           "</xsl:text><xsl:value-of select="@Name"/><xsl:text>",
 68:           false);
 69: </xsl:text>
 70:   </xsl:template>
 71:  
 72:   <xsl:template match="tns:Counter[@Type='RawFraction']" mode="InitCounter">
 73:     <xsl:text>
 74:         _</xsl:text><xsl:value-of select="@Symbol"/><xsl:text> = new PerformanceCounter(
 75:           CategoryName,
 76:           "</xsl:text><xsl:value-of select="@Name"/><xsl:text>",
 77:           false);
 78:  
 79:         _</xsl:text><xsl:value-of select="@Symbol"/><xsl:text>Base = new PerformanceCounter(
 80:           CategoryName,
 81:           "</xsl:text><xsl:value-of select="@Name"/><xsl:text> Base",
 82:           false);
 83: </xsl:text>
 84:   </xsl:template>
 85:  
 86:   <xsl:template match="tns:Counter" mode="CreateCounter">
 87:       <xsl:text>
 88:       {
 89:         CounterCreationData counter = new CounterCreationData();
 90:  
 91:         counter.CounterName = "</xsl:text><xsl:value-of select="@Name" /><xsl:text>";
 92:         counter.CounterHelp = "</xsl:text><xsl:value-of select="@Help" /><xsl:text>";
 93:         counter.CounterType = PerformanceCounterType.</xsl:text><xsl:value-of select="@Type" /><xsl:text>;
 94:  
 95:         counters.Add(counter);
 96:       }
 97: </xsl:text>
 98:   </xsl:template>
 99:  
100:   <xsl:template match="tns:Counter[@Type='RawFraction']" mode="CreateCounter">
101:       <xsl:text>
102:       {
103:         CounterCreationData counter = new CounterCreationData();
104:  
105:         counter.CounterName = "</xsl:text><xsl:value-of select="@Name" /><xsl:text>";
106:         counter.CounterHelp = "</xsl:text><xsl:value-of select="@Help" /><xsl:text>";
107:         counter.CounterType = PerformanceCounterType.</xsl:text><xsl:value-of select="@Type" /><xsl:text>;
108:  
109:         CounterCreationData counterBase = new CounterCreationData();
110:  
111:         counterBase.CounterName = "</xsl:text><xsl:value-of select="@Name" /><xsl:text> Base";
112:         counterBase.CounterHelp = "</xsl:text><xsl:value-of select="@Help" /><xsl:text>";
113:         counterBase.CounterType = PerformanceCounterType.RawBase;
114:  
115:         counters.AddRange(new CounterCreationData[]{counter, counterBase});
116:       }
117: </xsl:text>
118:   </xsl:template>
119:  
120:   <!-- ... -->
121: </xsl:stylesheet>

On lines 9 to 11 you'll find the declaration for the parameters which you can use to change the namespace, the class name and the access modifiers (public vs internal really) for the generated classes.

Starting on lines 22 and 37 respectively, you find different templates for general counters and the RawFraction type counters. I use this to automatically generate the base counter so you won't have to worry about it. You can do similar things for other counters which need a base counter. Then, starting on lines 63 and 72 respecctively, I initialize the counters, with specialization again for the RawFraction counter types. And starting on lines 86 and 100 respectively you'll find the code to create and set the CounterCreationData which is used to register perf counters.

You can find the full code in the ZIP file linked to at the end of this post. With this we basically have all the tools we need to actually generate the code as part of the build. We just need to bring the pieces together.

Step 4: Bringing it Together aka Updating the Project

Well yes, I have silently assumed that you have a c# project (.csproj or any other project that is supported by MSBuild.exe) that you're working on to update. All we need to do in the .csproj is to add the files we authored, then run the XSLT transformation and add the output .cs file to the list of source code files as well. And here's how you can do that.

 1: <?xml version="1.0" encoding="utf-8"?>
 2: <Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Build">
 3:   <!-- ... -->
 4:   <ItemGroup>
 5:     <PerfCounterXml Include="Diag\PerfCounters.xml">
 6:       <OutputCs>Diag\PerfCounters.Generated.cs</OutputCs>
 7:       <Parameters>
 8:         <Parameter Name="TargetNamespace" Value="$(RootNamespace).Diag" />
 9:       </Parameters>
10:       <SubType>Designer</SubType>
11:     </PerfCounterXml>
12:     <PerfCounterXslt Include="Diag\PerfCounters.xslt" />
13:     <Compile Include="Diag\PerfCounters.Generated.cs">
14:       <PerfCounters>true</PerfCounters>
15:       <AutoGen>true</AutoGen>
16:       <DependentUpon>PerfCounters.xml</DependentUpon>
17:     </Compile>
18:   </ItemGroup>
19:   <ItemGroup>
20:     <None Include="Diag\PerfCounters.xsd">
21:       <SubType>Designer</SubType>
22:     </None>
23:   </ItemGroup>
24:   <!-- XSL Transform for Perf Counters -->
25:   <Target Name="GeneratePerfCounters" BeforeTargets="BeforeBuild"
26:       Inputs="@(PerfCounterXml);@(PerfCounterXslt)"
27:       Outputs="@(PerfCounterXml->'%(OutputCs)')">
28:     <XslTransformation XmlInputPaths="@(PerfCounterXml)"
29:       XslInputPath="@(PerfCounterXslt)"
30:       OutputPaths="@(PerfCounterXml->'%(OutputCs)')"
31:       Parameters="@(PerfCounterXml->'%(Parameters)')" />
32:   </Target>
33: </Project>

Add lines 4 to 32 to the end of the project file and make sure the paths you're using point to the places where you actually stored the files. On line 8 you can see that I'm passing the target namespace parameter (called out in step 3) by concatenating the assemblies default root namespace and ".Diag". This way you can actually put this stuff into a .targets file that you can include in all of the projects which take advantage of this framework.

Starting on line 25 I defined the actual target which runs the XslTransformation task that comes with .net 4.0. I also declared the inputs as being the XML file and the XSLT file — this way, while experimenting with the XSLT, the code gets regenerated also if the XML file hasn't changed, but the XSLT has.

Using the Generated Classes

So now you have pretty much all the code you need to use these performance counters of yours. All that's left is actually writing the code which

  1. Installs the Performance Counters
  2. Updates the Performance Counters
  3. Uninstalls the Performance Counters

#1 and #3 would probably be used by your installer and #2 by your application at runtime. Incrementing and decrementing the counters is as easy as

1: PerfCounters.CymbeMail.ActiveConnections.Increment();
2: //...
3: PerfCounters.CymbeMail.ActiveConnections.Decrement();

Where PerfCounters is the name of the generated class which contains all the generated perf counters, CymbeMail is the name of the class for the category (the Symbol attribute, remember?) and ActiveConnections is of course the symbol name for the counter we're modifying. Isn't this simple?

As for setting things up (or removing them), the relevant piece of code gets generated for you, too. So all you need to do is actually call it, potentially from a small application that you run — as I mentioned before — from your installer.

1: // Setup the Performace Counters
2: PerfCounters.Setup();
3:  
4: // And for the Uninstaller:
5: // Remove the Performance Counters
6: PerfCounters.Remove();

Of course you'll have to make sure that the version of the generated code that's run by the uninstaller is the same version as the installer did run — else you may end up with stale categories / counters on the machine. But then again, if you're installing through an MSI package, you get that for free ... when done properly.

Summary

In just a few steps I have shown how you can build a framework to use performance counters in .Net applications. On top of it's simplicity, it's also quite easy to modify the framework and adapt it to your own needs or extend it to allow more performance counter types.

And finally, as promised, here's the ZIP file which contains the relevant pieces. Please forgive me for not adding the .csproj file — that actually contains other relevant data that I didn't want to share. Instead I added the generated c# code file for reference.

PerfCounters.zip (4.08 kb)

Sunday, May 09, 2010 6:47 PM by roger

I have updated my mail server code to report some very basic diagnostics data through performance counters (I'll come back to that in a future post). Amongst the information that's being logged are:

  • The total number of connections since the server was started.
  • The total number of connections refused (due to server and anti-spam policies) since the server was started.

I've looked at these numbers after a couple of hours of the server being online. There were about 60K connections, out of which a bit more than 75% have been refused SMTP transactions because the remote hosts were identified as spam senders.

Number of Connections to SMTP server.
Sunday, April 04, 2010 10:41 AM by roger

... the other day. It contained lots of material for me to read through and a form to fill in. But that's not the reason why I'm typing this.

In order to still participate in certain elections and votes back in Switzerland, I have to register with the Swiss consulate in San Francisco. After doing so, they sent me a letter with additional info and forms. What I find most intriguing is that, even though they're based in the US, they must have had their paper and envelopes shipped from Switzerland.

Monday, March 29, 2010 8:43 PM by roger

This is surely no surprise for many people: you can actually debug managed code with WinDbg (or CDB, if you prefer that one) by using the SOS extension (that ships with the .Net framework) in the debugger. What's new is that today, Microsoft released Psscor2 which is pretty much an enhanced version of SOS. Now I'm sure that you'll soon find much information on using Psscor2 on Tom's blog. I'm much in favor of Psscor2 myself and therefore I'll try to post useful scripts here as well (if I can come up with some ;-)).

Tuesday, March 02, 2010 11:57 PM by roger

I decided to start with a new series of posts (oh well, I guess it'll turn out later if it's really going to be a series) of Here's to ... Xyz posts to praise (or slam; go figure it out!) stuff.

So here's to changes. If someone told me a year ago where I would be in a year and what I would be doing, I would have smiled at them and thought something like 'Yeah, whatever.' But here I am: After almost 4 years in Microsoft's Development Center in Zurich, Switzerland I decided to pursue new opportunities and to move to Redmond in the state of Washington, where I am now working as part of the Windows Services and Content team in the Windows client organization. Which gets me right to the point: Changes. For me this really was a big change and so far I think it was well worth it. I have to admit though, that I did underestimate the work and stress invovled in the relocation (and I'm not completely done yet). On the other hand, what I gained so far already is priceless. So maybe in a year from now, you should ask me what I think about this. And maybe I'll still tell you the same thing. Changes can be a good thing, if you give it a try.

Sunday, February 07, 2010 12:42 AM by roger

... in order to pick up the changes you've recently made to the environment variables.

Alright, to cut a long story short, I'm using the Microsoft Natural Ergonomic Keyboard at work. It has these (5) 'My Favorits' keys on top to which you can assign custom commands. I typically make heavy use of that to

  1. Start a new console for the build environment
  2. Start PowerShell
  3. Start a lightweight editor with syntax hightlighting for many file types
  4. Start PaintDotNet
  5. Start a new (blank) console

I typically use #5 for checking out small things and starting remote user debugging sessions. From time to time, I tweak the environment a little bit by adding new environment variables or modifying existing ones (like e.g. adding new places to the %PATH% environment variable). To verify, I use the command line. Except that existing processes don't automatically get the new variables. I was recently tricked by this, since I pressed #5 to get a new console and test the changes. Nothing there. Hmmm. When I start %COMSPEC% they are there. So why not with #5?

Well, the process which starts #5 was started when the changes to the environment variables where not yet made. That's why also the processes started from this process don't have the changes. So all I've got to do in this case is to kill itype.exe and then start it right away again. Now it works!

Friday, December 11, 2009 2:13 PM by roger

Tracing OCS components may be vital in troubleshooting various issues you may face in your deployment. On machines where you have OCS components installed, you'll typically find a tool called OCSLogger.exe which allows you to start/stop/view traces of OCS components. However, sometimes this is not enough, for instance when you see problems at the startup of a machine. It's kind of hard to run the GUI if you cannot logon yet. But you can typically run a scheduled task. Or maybe you are — just like me — more like the console guy and thus want to have a script/cmdline tool for everything.

Let's start with the config file used by the script (TraceConfig.xml) which defines the components you want to trace, to what level the traces are supposed to be and some more things. The sample given here traces mostly the components which are useful in troubleshooting issues related to the Response Group Service of OCS.

 1: <?xml version="1.0" encoding="utf-8"?>
 2: <Config>
 3:     <!--
 4:         Levels:
 5:             TL_FATAL        1
 6:             TL_ERROR        2
 7:             TL_WARN         3
 8:             TL_INFO         4
 9:             TL_VERBOSE      5
10:             TL_NOISE        6
11:
12:         Flags:
13:             TF_COMPONENT    0x00000001
14:             TF_PROTOCOL     0x00000002
15:             TF_CONNECTION   0x00000004
16:             TF_SECURITY     0x00000008
17:             TF_DIAG         0x00000010
18:             TF_AUTH         0x00000020
19:             TF_PARSE        0x00000040
20:             TF_NETWORK      0x00000080
21:             TF_STACKTRACE   0x00000100
22:     -->
23:     <Default Level="6" Flags="0xffff" />
24:     <Paths Tracer="C:\Program Files\Common Files\Microsoft Communications Server 2007 R2\Tracing"
25:            Etl="D:\Tracing"
26:            Log="D:\Tracing"
27:            TmfSearchPath="C:\Program Files\Common Files\Microsoft Communications Server 2007 R2\Tracing">
28:     </Paths>
29:     <Components>
30:         <Component Name="LcsWMI" Enabled="no" />
31:         <Component Name="LcsWMIUserServices" Enabled="no" />
32:  
33:         <Component Name="PowerShell" Enabled="yes" />
34:  
35:         <Component Name="ApplicationServer" Enabled="yes" />
36:  
37:         <Component Name="RgsClientsLib" Enabled="yes" />
38:         <Component Name="RgsCommonLibrary" Enabled="yes" />
39:         <Component Name="RgsDatastores" Enabled="yes" />
40:         <Component Name="RgsDeploymentApi" Enabled="yes" />
41:         <Component Name="RgsDeploymentLibrary" Enabled="yes" />
42:         <Component Name="RgsDiagnostics" Enabled="yes" />
43:         <Component Name="RgsHostingFramework" Enabled="yes" />
44:         <Component Name="RgsMatchMakingService" Enabled="yes" />
45:     </Components>
46: </Config>

I added the most importan trace levels and flags in the comment. Right now, the Default element defines the levels and flags for all components, but there's no reason why you shouldn't be able to do that per component you want to trace.

The PS1 script itself (Tracer.ps1) heavily relies on the OcsTracer.exe tool which also comes with OCS and is typically installed in the same place as OcsLogger.exe. It has four main actions:

  1. Start tracing components
  2. Stop tracing components and format the traces
  3. Format traces of ETL files (e.g. from a different machine)
  4. Show the configuration details from a particular config XML file

  1: <#
  2: .SYNOPSIS
  3:         Starts or Stops tracing of Office Communications Server components.
  4: .DESCRIPTION
  5:         Starts or Stops tracing of Office Communications Server components.
  6: .PARAMETER Action
  7:         The action to perform. Must be one of 'Start', 'Stop', 'Config' or
  8:         'Format'.
  9: .PARAMETER ConfigPath
 10:         The path to the configuration XML file. If not specified,
 11:         "TraceConfig.xml" is used.
 12: .LINK
 13:         This script was originally posted to
 14:         http://www.cymbeline.ch/post/2009/12/11/PowerShell-Script-to-trace-OCS-Components.aspx
 15: .EXAMPLE
 16:         .\Tracer.ps1 Start
 17:
 18:         Starts tracing all the enabled components from the "TraceConfig.xml" file.
 19: .EXAMPLE
 20:         .\Tracer.ps1 Stop
 21:
 22:         Stops tracing all the enabled components from the "TraceConfig.xml" file
 23:         and formats the traces.
 24: .EXAMPLE
 25:         .\Tracer.ps1 Format "MyOtherConfig.xml"
 26:
 27:         Formats the traces of the enabled components from the "MyOtherConfig.xml"
 28:         file with all the settings from the "MyOtherConfig.xml" file.
 29: .EXAMPLE
 30:         .\Tracer.ps1 Config
 31:
 32:         Shows the configuration of the "TraceConfig.xml" file.
 33: #>
 34: param(
 35:     [Parameter(Mandatory=$true)]
 36:     [ValidateSet("Start", "Stop", "Config", "Format", IgnoreCase=$true)]
 37:     [String] $Action,
 38:     [String] $ConfigPath = "TraceConfig.xml"
 39: )
 40:  
 41: $configXml = ([xml](Get-Content $ConfigPath))
 42: $tracerPath = $configXml.Config.Paths.Tracer
 43: $etlDir = $configXml.Config.Paths.Etl
 44: $logDir = $configXml.Config.Paths.Log
 45: $tmfSearchPath = $configXml.Config.Paths.TmfSearchPath
 46:  
 47: # Construct the parameters for the 'Start' command to OcsTracer.exe
 48: function getStartParams()
 49: {
 50:     $ret = @()
 51:  
 52:     $configXml.Config.Components.Component |
 53:         ? {$_.Enabled -eq "yes"} |
 54:         foreach {
 55:             $ret = $ret +
 56:                 ("/Component:" + $_.Name + "," + $configXml.Config.Default.Level +
 57:                     "," + $configXml.Config.Default.Flags + " ")
 58:         }
 59:  
 60:     return $ret
 61: }
 62:  
 63: # Construct the parameters for the 'Stop' command to OcsTracer.exe
 64: function getStopParams()
 65: {
 66:     $ret = @()
 67:  
 68:     $configXml.Config.Components.Component |
 69:         ? {$_.Enabled -eq "yes"} |
 70:         foreach { $ret = $ret + ("/Component:" + $_.Name) }
 71:  
 72:     return $ret
 73: }
 74:  
 75: # Format the ETL files for enabled components to a human readable format
 76: function formatFiles(
 77:     [Parameter(Mandatory=$true)]
 78:     [String] $Timestamp
 79: )
 80: {
 81:     md $logDir\$timestamp -ea silentlycontinue | Out-Null
 82:  
 83:     $configXml.Config.Components.Component |
 84:         ? {$_.Enabled -eq "yes"} |
 85:         foreach {
 86:             $etlFile = $_.Name + ".etl";
 87:  
 88:             if (Test-Path $etlFile)
 89:             {
 90:                 $logFile = $Timestamp + "\" + $Timestamp + "_" + $_.Name + ".log";
 91:  
 92:                 & "$tracerPath\OcsTracer.exe" Format /LogFilePath:"$etlDir\$etlFile" /OutputFile:"$logDir\$logFile" /TmfSearchPath:"$tmfSearchPath" | Write-Verbose
 93:             }
 94:             else
 95:             {
 96:                 Write-Warning "File $etlFile not found.";
 97:             }
 98:         }
 99: }
100:  
101: Write-Host "Using Config File: $ConfigPath"
102: $timestamp = Get-Date -format "yyyy-MM-dd_HH.mm.ss"
103:  
104: if ($Action -eq "start")
105: {
106:     Write-Host "Removing all .etl files ..."
107:     ls $etlDir *.etl | ri
108:  
109:     Write-Host "Start tracing components ..."
110:     $params = getStartParams
111:  
112:     & "$tracerPath\OcsTracer.exe" Start $params /LogFileFolder:"$etlDir" | Write-Verbose
113: }
114: elseif ($Action -eq "stop")
115: {
116:     Write-Host "Stop tracing components ..."
117:     $params = getStopParams
118:  
119:     md $logDir\$timestamp | Out-Null
120:  
121:     & "$tracerPath\OcsTracer.exe" Stop $params /OutputFile:"$logDir\$timestamp\$($timestamp)_All.log" /TmfSearchPath:"$tmfSearchPath" | Write-Verbose
122:  
123:     if (!$?)
124:     {
125:         rd $logDir\$timestamp | Out-Null
126:     }
127:     else
128:     {
129:         Write-Host "Sessions stopped. Start formatting ..."
130:         formatFiles $timestamp
131:     }
132: }
133: elseif ($Action -eq "format")
134: {
135:     Write-Host "Formatting traces from ETL files ..."
136:     formatFiles $timestamp
137: }
138: elseif ($Action -eq "config")
139: {
140:     Write-Host "Default values"
141:     Write-Host "--------------"
142:     $configXml.Config.Default | ft Level,Flags
143:  
144:     Write-Host "Paths"
145:     Write-Host "-----"
146:     $configXml.Config.Paths | fl
147:  
148:     Write-Host "Components"
149:     Write-Host "----------"
150:     $configXml.Config.Components.Component | ft Name,Enabled
151: }
152: else
153: {
154:     Write-Error "Unknown action."
155: }

For samples on how to run the script, please run man .\Tracer.ps1 -Examples Have fun :)

Monday, November 30, 2009 7:54 PM by roger

At work, I have created multiple tools which we used to analyse and fix issues related to certificates that we use with Office Communications Server and their respective private key files. To summarize, a user/service who wants to use the certificate for authentication needs to have read access on the private key. If it doesn't, you'll typically see a strange error which many people don't relate to missing ACLs on the private key file.

Now these days, you don't need to write such tools anymore. PowerShell allows you to pretty much do everything you need in this area. Let's look at the following PS script (let's call it FindPrivateKey.ps1) which accepts a parameter of type System.Security.Cryptography.X509Certificates.X509Certificate2, i.e. a reference to the certificate you want to analyze.

 1: param(
 2:     [Parameter(Mandatory=$true)]
 3:     [System.Security.Cryptography.X509Certificates.X509Certificate2]
 4:     $Certificate
 5: )
 6:  
 7: echo "Looking for private key file of certificate"
 8: echo $Certificate
 9: echo ""
10: echo "The private key file is '$($Certificate.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName)'"
11: echo ""
12:  
13: $file = ls $env:userprofile -Filter $Certificate.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName -Recurse -Force -EA SilentlyContinue
14: echo "It is located at '$($file.FullName)'."

I guess I do not need to mention that you can easily find the certificate you're interested in by running something like

1: cd cert:\CurrentUser\My
2: ls
3: $cert = gci 0DAC31905AEB722D8561BFAF3F3BFD2F551AA197
4: FindPrivateKey.ps1 $cert

where '0DAC31905AEB722D8561BFAF3F3BFD2F551AA197' is simply the thumbprint of the certificate we're interested in. From here on it should be easy to check what ACLs the file has (run $file.GetAccessControl()) and to modify them (run $file.SetAccessControl()).

 
About
Hi, my name is Roger Keller and I am a software engineer from Zürich, Switzerland living in Redmond, WA. I work in the Windows Services and Content group which is part of the Windows client team in Microsoft.